Import of bazel plugin using copybara

PiperOrigin-RevId: 148474984
diff --git a/WORKSPACE b/WORKSPACE
index bad62f5..bffcb5e 100644
--- a/WORKSPACE
+++ b/WORKSPACE
@@ -50,6 +50,14 @@
     url = "https://dl.google.com/dl/android/studio/ide-zips/2.3.0.4/android-studio-ide-162.3616766-linux.zip",
 )
 
+# The plugin api for Android Studio 2.3 Beta 4. This is required to build ASwB,
+# and run integration tests.
+new_http_archive(
+    name = "android_studio_2_3_0_6",
+    build_file = "intellij_platform_sdk/BUILD.android_studio",
+    url = "https://dl.google.com/dl/android/studio/ide-zips/2.3.0.6/android-studio-ide-162.3715353-linux.zip",
+)
+
 # The plugin api for Android Studio 2.2 stable. This is required to build ASwB,
 # and run integration tests.
 new_http_archive(
diff --git a/aswb/2.2/tests/utils/integration/com/google/idea/blaze/android/AndroidTestCleanupHelper.java b/aswb/2.2/tests/utils/integration/com/google/idea/blaze/android/AndroidIntegrationTestCleanupHelper.java
similarity index 97%
rename from aswb/2.2/tests/utils/integration/com/google/idea/blaze/android/AndroidTestCleanupHelper.java
rename to aswb/2.2/tests/utils/integration/com/google/idea/blaze/android/AndroidIntegrationTestCleanupHelper.java
index 6847c67..d1b4a07 100644
--- a/aswb/2.2/tests/utils/integration/com/google/idea/blaze/android/AndroidTestCleanupHelper.java
+++ b/aswb/2.2/tests/utils/integration/com/google/idea/blaze/android/AndroidIntegrationTestCleanupHelper.java
@@ -32,7 +32,7 @@
  * TODO: Remove once we build for a version where post-initialization insight and style settings are
  * used in the settings check.
  */
-public final class AndroidTestCleanupHelper {
+public final class AndroidIntegrationTestCleanupHelper {
 
   public static void cleanUp(Project project) {
     resetCodeInsightSettings();
diff --git a/aswb/2.2/tests/utils/integration/com/google/idea/blaze/android/AndroidTestSetupRule.java b/aswb/2.2/tests/utils/integration/com/google/idea/blaze/android/AndroidIntegrationTestSetupRule.java
similarity index 91%
rename from aswb/2.2/tests/utils/integration/com/google/idea/blaze/android/AndroidTestSetupRule.java
rename to aswb/2.2/tests/utils/integration/com/google/idea/blaze/android/AndroidIntegrationTestSetupRule.java
index 07fc1ac..293643f 100644
--- a/aswb/2.2/tests/utils/integration/com/google/idea/blaze/android/AndroidTestSetupRule.java
+++ b/aswb/2.2/tests/utils/integration/com/google/idea/blaze/android/AndroidIntegrationTestSetupRule.java
@@ -23,7 +23,7 @@
  * Runs before Android Studio integration tests, to ensure the AndroidStudio platform prefix is
  * honored.
  */
-public class AndroidTestSetupRule extends ExternalResource {
+public class AndroidIntegrationTestSetupRule extends ExternalResource {
 
   @Override
   protected void before() throws Throwable {
@@ -33,6 +33,7 @@
     // a matching descriptor for one of them. Notably, "AndroidStudio" is not a candidate.
     // The first parameter doesn't matter in our case, so we pass a nonexistent class name.
     PlatformTestCase.initPlatformPrefix("", System.getProperty(PlatformUtils.PLATFORM_PREFIX_KEY));
+    System.setProperty("android.studio.sdk.manager.disabled", "true");
     // TODO: Remove the above once we build for a version where "AndroidStudio" is a candidate.
   }
 }
diff --git a/aswb/2.2/tests/utils/integration/com/google/idea/blaze/android/BlazeAndroidIntegrationTestCase.java b/aswb/2.2/tests/utils/integration/com/google/idea/blaze/android/BlazeAndroidIntegrationTestCase.java
deleted file mode 100644
index 106860a..0000000
--- a/aswb/2.2/tests/utils/integration/com/google/idea/blaze/android/BlazeAndroidIntegrationTestCase.java
+++ /dev/null
@@ -1,32 +0,0 @@
-/*
- * Copyright 2016 The Bazel Authors. All rights reserved.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.google.idea.blaze.android;
-
-import com.google.idea.blaze.base.BlazeIntegrationTestCase;
-import org.junit.After;
-import org.junit.Rule;
-
-/** Base test class for Blaze Android integration tests. */
-public abstract class BlazeAndroidIntegrationTestCase extends BlazeIntegrationTestCase {
-
-  @Rule public final AndroidTestSetupRule androidSetupRule = new AndroidTestSetupRule();
-
-  @After
-  public final void doTeardown() {
-    AndroidTestCleanupHelper.cleanUp(getProject());
-  }
-}
diff --git a/aswb/2.3/tests/unittests/com/google/idea/blaze/android/project/BlazeBuildSystemServiceTest.java b/aswb/2.3/tests/unittests/com/google/idea/blaze/android/project/BlazeBuildSystemServiceTest.java
index fbd398d..ce41514 100755
--- a/aswb/2.3/tests/unittests/com/google/idea/blaze/android/project/BlazeBuildSystemServiceTest.java
+++ b/aswb/2.3/tests/unittests/com/google/idea/blaze/android/project/BlazeBuildSystemServiceTest.java
@@ -165,7 +165,7 @@
   }
 
   private void mockBlazeImportSettings(Container projectServices) {
-    BlazeImportSettingsManager importSettingsManager = new BlazeImportSettingsManager(project);
+    BlazeImportSettingsManager importSettingsManager = new BlazeImportSettingsManager();
     importSettingsManager.setImportSettings(
         new BlazeImportSettings("", "", "", "", "", Blaze.BuildSystem.Blaze));
     projectServices.register(BlazeImportSettingsManager.class, importSettingsManager);
diff --git a/aswb/2.3/tests/unittests/com/google/idea/blaze/android/project/BlazeFeatureEnabledServiceTest.java b/aswb/2.3/tests/unittests/com/google/idea/blaze/android/project/BlazeFeatureEnabledServiceTest.java
index efd77ea..a33eb31 100644
--- a/aswb/2.3/tests/unittests/com/google/idea/blaze/android/project/BlazeFeatureEnabledServiceTest.java
+++ b/aswb/2.3/tests/unittests/com/google/idea/blaze/android/project/BlazeFeatureEnabledServiceTest.java
@@ -27,7 +27,6 @@
 import com.google.idea.blaze.base.settings.Blaze.BuildSystem;
 import com.google.idea.blaze.base.settings.BlazeImportSettings;
 import com.google.idea.blaze.base.settings.BlazeImportSettingsManager;
-import com.google.idea.blaze.base.settings.BlazeImportSettingsManagerLegacy;
 import com.google.idea.blaze.base.sync.data.BlazeProjectDataManager;
 import com.google.idea.common.experiments.ExperimentService;
 import com.intellij.openapi.extensions.ExtensionPoint;
@@ -59,12 +58,10 @@
     projectDataManager = new MockBlazeProjectDataManager();
     projectServices.register(BlazeProjectDataManager.class, projectDataManager);
 
-    BlazeImportSettingsManager importSettingsManager = new BlazeImportSettingsManager(project);
+    BlazeImportSettingsManager importSettingsManager = new BlazeImportSettingsManager();
     importSettingsManager.setImportSettings(
         new BlazeImportSettings(null, null, null, null, null, BuildSystem.Blaze));
     projectServices.register(BlazeImportSettingsManager.class, importSettingsManager);
-    projectServices.register(
-        BlazeImportSettingsManagerLegacy.class, new BlazeImportSettingsManagerLegacy(project));
 
     ExtensionPoint<FeatureEnableService> extensionPoint =
         registerExtensionPoint(
diff --git a/aswb/2.3/tests/unittests/com/google/idea/blaze/android/rendering/BlazeRenderErrorContributorTest.java b/aswb/2.3/tests/unittests/com/google/idea/blaze/android/rendering/BlazeRenderErrorContributorTest.java
index ffe8267..ee48a99 100644
--- a/aswb/2.3/tests/unittests/com/google/idea/blaze/android/rendering/BlazeRenderErrorContributorTest.java
+++ b/aswb/2.3/tests/unittests/com/google/idea/blaze/android/rendering/BlazeRenderErrorContributorTest.java
@@ -44,7 +44,6 @@
 import com.google.idea.blaze.base.settings.Blaze.BuildSystem;
 import com.google.idea.blaze.base.settings.BlazeImportSettings;
 import com.google.idea.blaze.base.settings.BlazeImportSettingsManager;
-import com.google.idea.blaze.base.settings.BlazeImportSettingsManagerLegacy;
 import com.google.idea.blaze.base.sync.data.BlazeProjectDataManager;
 import com.google.idea.blaze.base.sync.workspace.ArtifactLocationDecoder;
 import com.google.idea.blaze.base.targetmaps.SourceToTargetMap;
@@ -103,12 +102,10 @@
     projectServices.register(
         AndroidResourceModuleRegistry.class, new AndroidResourceModuleRegistry());
 
-    BlazeImportSettingsManager importSettingsManager = new BlazeImportSettingsManager(project);
+    BlazeImportSettingsManager importSettingsManager = new BlazeImportSettingsManager();
     BlazeImportSettings settings = new BlazeImportSettings("", "", "", "", "", BuildSystem.Blaze);
     importSettingsManager.setImportSettings(settings);
     projectServices.register(BlazeImportSettingsManager.class, importSettingsManager);
-    projectServices.register(
-        BlazeImportSettingsManagerLegacy.class, new BlazeImportSettingsManagerLegacy(project));
 
     createPsiClassesAndSourceToTargetMap(projectServices);
 
diff --git a/aswb/2.3/tests/unittests/com/google/idea/blaze/android/run/testrecorder/BlazeConfigurationsTest.java b/aswb/2.3/tests/unittests/com/google/idea/blaze/android/run/testrecorder/BlazeConfigurationsTest.java
index 86c20cf..e5a828d 100644
--- a/aswb/2.3/tests/unittests/com/google/idea/blaze/android/run/testrecorder/BlazeConfigurationsTest.java
+++ b/aswb/2.3/tests/unittests/com/google/idea/blaze/android/run/testrecorder/BlazeConfigurationsTest.java
@@ -164,7 +164,7 @@
   }
 
   private void mockBlazeImportSettings(Container projectServices) {
-    BlazeImportSettingsManager importSettingsManager = new BlazeImportSettingsManager(project);
+    BlazeImportSettingsManager importSettingsManager = new BlazeImportSettingsManager();
     importSettingsManager.setImportSettings(
         new BlazeImportSettings("", "", "", "", "", Blaze.BuildSystem.Blaze));
     projectServices.register(BlazeImportSettingsManager.class, importSettingsManager);
@@ -251,6 +251,11 @@
     }
 
     @Override
+    public String getBinaryPath() {
+      return "/usr/bin/blaze";
+    }
+
+    @Override
     public WorkspaceRootProvider getWorkspaceRootProvider() {
       return null;
     }
@@ -278,8 +283,8 @@
     }
 
     @Override
-    public FileNameMatcher buildFileMatcher() {
-      return null;
+    public ImmutableList<FileNameMatcher> buildFileMatchers() {
+      return ImmutableList.of();
     }
 
     @Override
diff --git a/aswb/2.3/tests/utils/integration/com/google/idea/blaze/android/BlazeAndroidIntegrationTestCase.java b/aswb/2.3/tests/utils/integration/com/google/idea/blaze/android/AndroidIntegrationTestCleanupHelper.java
similarity index 64%
copy from aswb/2.3/tests/utils/integration/com/google/idea/blaze/android/BlazeAndroidIntegrationTestCase.java
copy to aswb/2.3/tests/utils/integration/com/google/idea/blaze/android/AndroidIntegrationTestCleanupHelper.java
index 74459a0..5f425ca 100644
--- a/aswb/2.3/tests/utils/integration/com/google/idea/blaze/android/BlazeAndroidIntegrationTestCase.java
+++ b/aswb/2.3/tests/utils/integration/com/google/idea/blaze/android/AndroidIntegrationTestCleanupHelper.java
@@ -16,7 +16,15 @@
 
 package com.google.idea.blaze.android;
 
-import com.google.idea.blaze.base.BlazeIntegrationTestCase;
+import com.intellij.openapi.project.Project;
 
-/** Compatibility test class for Blaze Android integration tests. */
-public abstract class BlazeAndroidIntegrationTestCase extends BlazeIntegrationTestCase {}
+/**
+ * Helper class for cleaning up after Android Studio initialization.
+ *
+ * <p>This class exists only for 2.2 compatibility and can be removed once 2.2 support is removed.
+ */
+public final class AndroidIntegrationTestCleanupHelper {
+
+  @SuppressWarnings("unused")
+  public static void cleanUp(Project project) {}
+}
diff --git a/aswb/2.3/tests/utils/integration/com/google/idea/blaze/android/BlazeAndroidIntegrationTestCase.java b/aswb/2.3/tests/utils/integration/com/google/idea/blaze/android/AndroidIntegrationTestSetupRule.java
similarity index 68%
rename from aswb/2.3/tests/utils/integration/com/google/idea/blaze/android/BlazeAndroidIntegrationTestCase.java
rename to aswb/2.3/tests/utils/integration/com/google/idea/blaze/android/AndroidIntegrationTestSetupRule.java
index 74459a0..088e953 100644
--- a/aswb/2.3/tests/utils/integration/com/google/idea/blaze/android/BlazeAndroidIntegrationTestCase.java
+++ b/aswb/2.3/tests/utils/integration/com/google/idea/blaze/android/AndroidIntegrationTestSetupRule.java
@@ -13,10 +13,15 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-
 package com.google.idea.blaze.android;
 
-import com.google.idea.blaze.base.BlazeIntegrationTestCase;
+import org.junit.rules.ExternalResource;
 
-/** Compatibility test class for Blaze Android integration tests. */
-public abstract class BlazeAndroidIntegrationTestCase extends BlazeIntegrationTestCase {}
+/** Runs before Android Studio integration tests. */
+public class AndroidIntegrationTestSetupRule extends ExternalResource {
+
+  @Override
+  protected void before() throws Throwable {
+    System.setProperty("android.studio.sdk.manager.disabled", "true");
+  }
+}
diff --git a/aswb/BUILD b/aswb/BUILD
index 16fc99b..7fe4a00 100644
--- a/aswb/BUILD
+++ b/aswb/BUILD
@@ -25,6 +25,7 @@
         "android-studio-145.1617.8": [],
         "android-studio-2.3.0.3": ["2.3/src/META-INF/aswb_beta.xml"],
         "android-studio-2.3.0.4": ["2.3/src/META-INF/aswb_beta.xml"],
+        "android-studio-2.3.0.6": ["2.3/src/META-INF/aswb_beta.xml"],
     }),
     visibility = [
         "//visibility:public",
@@ -56,6 +57,7 @@
         "android-studio-145.1617.8": glob(["2.2/src/**/*.java"]),
         "android-studio-2.3.0.3": glob(["2.3/src/**/*.java"]),
         "android-studio-2.3.0.4": glob(["2.3/src/**/*.java"]),
+        "android-studio-2.3.0.6": glob(["2.3/src/**/*.java"]),
     }),
     resources = glob(["resources/**/*"]),
     visibility = [
@@ -67,7 +69,7 @@
         "//cpp",
         "//intellij_platform_sdk:plugin_api",
         "//java",
-        "//proto_deps",
+        "//proto:proto_deps",
         "@jsr305_annotations//jar",
     ],
 )
@@ -80,6 +82,7 @@
         "android-studio-145.1617.8": glob(["2.2/tests/utils/integration/**/*.java"]),
         "android-studio-2.3.0.3": glob(["2.3/tests/utils/integration/**/*.java"]),
         "android-studio-2.3.0.4": glob(["2.3/tests/utils/integration/**/*.java"]),
+        "android-studio-2.3.0.6": glob(["2.3/tests/utils/integration/**/*.java"]),
     }),
     deps = [
         "//base",
@@ -104,6 +107,7 @@
         "android-studio-145.1617.8": [],
         "android-studio-2.3.0.3": glob(["2.3/tests/unittests/**/*.java"]),
         "android-studio-2.3.0.4": glob(["2.3/tests/unittests/**/*.java"]),
+        "android-studio-2.3.0.6": glob(["2.3/tests/unittests/**/*.java"]),
     }),
     test_package_root = "com.google.idea.blaze.android",
     deps = [
@@ -114,7 +118,7 @@
         "//common/experiments:unit_test_utils",
         "//intellij_platform_sdk:plugin_api_for_tests",
         "//java",
-        "//proto_deps",
+        "//proto:proto_deps",
         "@jsr305_annotations//jar",
         "@junit//jar",
     ],
@@ -127,6 +131,7 @@
         "android-studio-145.1617.8": [],
         "android-studio-2.3.0.3": glob(["2.3/tests/integrationtests/**/*.java"]),
         "android-studio-2.3.0.4": glob(["2.3/tests/integrationtests/**/*.java"]),
+        "android-studio-2.3.0.6": glob(["2.3/tests/integrationtests/**/*.java"]),
     }),
     platform_prefix = "AndroidStudio",
     required_plugins = "com.google.idea.bazel.aswb",
@@ -144,7 +149,7 @@
         "//common/experiments:unit_test_utils",
         "//intellij_platform_sdk:plugin_api_for_tests",
         "//java",
-        "//proto_deps",
+        "//proto:proto_deps",
         "@jsr305_annotations//jar",
         "@junit//jar",
     ],
diff --git a/aswb/src/META-INF/aswb.xml b/aswb/src/META-INF/aswb.xml
index d59d935..b4b61ab 100644
--- a/aswb/src/META-INF/aswb.xml
+++ b/aswb/src/META-INF/aswb.xml
@@ -40,6 +40,9 @@
     <projectService serviceImplementation="com.google.idea.blaze.android.manifest.ManifestParser"/>
     <projectService serviceImplementation="com.google.idea.blaze.android.sync.model.AndroidResourceModuleRegistry"/>
     <applicationService serviceImplementation="com.google.idea.blaze.android.settings.BlazeAndroidUserSettings"/>
+    <applicationService serviceInterface="com.google.idea.blaze.android.sdk.BlazeSdkProvider"
+      serviceImplementation="com.google.idea.blaze.android.sdk.BlazeSdkProviderImpl"
+      testServiceImplementation="com.google.idea.blaze.android.sdk.MockBlazeSdkProvider"/>
   </extensions>
 
   <extensions defaultExtensionNs="org.jetbrains.android.actions">
diff --git a/aswb/src/com/google/idea/blaze/android/cppimpl/BlazeNdkSupportEnabler.java b/aswb/src/com/google/idea/blaze/android/cppimpl/BlazeNdkSupportEnabler.java
index f6b50ae..75e1a12 100644
--- a/aswb/src/com/google/idea/blaze/android/cppimpl/BlazeNdkSupportEnabler.java
+++ b/aswb/src/com/google/idea/blaze/android/cppimpl/BlazeNdkSupportEnabler.java
@@ -25,11 +25,10 @@
 import com.google.idea.blaze.base.settings.BlazeImportSettings;
 import com.google.idea.blaze.base.sync.BlazeSyncParams.SyncMode;
 import com.google.idea.blaze.base.sync.SyncListener;
-import com.google.idea.blaze.cpp.BlazeCWorkspace;
 import com.intellij.openapi.application.ApplicationManager;
 import com.intellij.openapi.project.Project;
 import com.jetbrains.cidr.lang.workspace.OCWorkspace;
-import org.jetbrains.annotations.NotNull;
+import com.jetbrains.cidr.lang.workspace.OCWorkspaceManager;
 
 final class BlazeNdkSupportEnabler extends SyncListener.Adapter {
 
@@ -57,7 +56,7 @@
    * @param enabled if true, turn on C support in the IDE. If false, turn off C support in the IDE.
    */
   private static void enableCSupportInIde(Project project, boolean enabled) {
-    BlazeCWorkspace workspace = BlazeCWorkspace.getInstance(project);
+    OCWorkspace workspace = OCWorkspaceManager.getWorkspace(project);
     Boolean isCurrentlyEnabled = !LANGUAGE_SUPPORT_DISABLED.get(project, false);
     if (isCurrentlyEnabled != enabled) {
       NdkHelper.disableCppLanguageSupport(project, !enabled);
@@ -65,7 +64,7 @@
     }
   }
 
-  private static void rebuildSymbols(@NotNull Project project, @NotNull OCWorkspace workspace) {
+  private static void rebuildSymbols(Project project, OCWorkspace workspace) {
     ApplicationManager.getApplication()
         .runReadAction(
             () -> {
diff --git a/aswb/src/com/google/idea/blaze/android/projectview/AndroidSdkPlatformSection.java b/aswb/src/com/google/idea/blaze/android/projectview/AndroidSdkPlatformSection.java
index fe75735..a9cbded 100644
--- a/aswb/src/com/google/idea/blaze/android/projectview/AndroidSdkPlatformSection.java
+++ b/aswb/src/com/google/idea/blaze/android/projectview/AndroidSdkPlatformSection.java
@@ -18,7 +18,7 @@
 import static java.util.stream.Collectors.toList;
 
 import com.google.common.collect.ImmutableList;
-import com.google.idea.blaze.android.compatibility.Compatibility.AndroidSdkUtils;
+import com.google.idea.blaze.android.sdk.BlazeSdkProvider;
 import com.google.idea.blaze.android.sync.sdk.AndroidSdkFromProjectView;
 import com.google.idea.blaze.base.projectview.ProjectView;
 import com.google.idea.blaze.base.projectview.parser.ParseContext;
@@ -66,7 +66,7 @@
       if (!projectView.getSectionsOfType(KEY).isEmpty()) {
         return projectView;
       }
-      List<Sdk> sdks = AndroidSdkUtils.getAllAndroidSdks();
+      List<Sdk> sdks = BlazeSdkProvider.getInstance().getAllAndroidSdks();
       ProjectView.Builder builder =
           ProjectView.builder(projectView).add(TextBlockSection.of(TextBlock.newLine()));
 
@@ -81,7 +81,7 @@
       } else if (sdks.size() == 1) {
         builder.add(
             ScalarSection.builder(KEY)
-                .set(AndroidSdkFromProjectView.getSdkTargetHash(sdks.get(0))));
+                .set(BlazeSdkProvider.getInstance().getSdkTargetHash(sdks.get(0))));
       } else {
         builder.add(
             TextBlockSection.of(
diff --git a/aswb/src/com/google/idea/blaze/android/run/binary/mobileinstall/BlazeApkBuildStepMobileInstall.java b/aswb/src/com/google/idea/blaze/android/run/binary/mobileinstall/BlazeApkBuildStepMobileInstall.java
index 1256ff4..2082b77 100644
--- a/aswb/src/com/google/idea/blaze/android/run/binary/mobileinstall/BlazeApkBuildStepMobileInstall.java
+++ b/aswb/src/com/google/idea/blaze/android/run/binary/mobileinstall/BlazeApkBuildStepMobileInstall.java
@@ -23,11 +23,11 @@
 import com.google.common.util.concurrent.ListenableFuture;
 import com.google.common.util.concurrent.SettableFuture;
 import com.google.common.util.concurrent.UncheckedExecutionException;
-import com.google.idea.blaze.android.compatibility.Compatibility.AndroidSdkUtils;
 import com.google.idea.blaze.android.run.deployinfo.BlazeAndroidDeployInfo;
 import com.google.idea.blaze.android.run.deployinfo.BlazeApkDeployInfoProtoHelper;
 import com.google.idea.blaze.android.run.runner.BlazeAndroidDeviceSelector;
 import com.google.idea.blaze.android.run.runner.BlazeApkBuildStep;
+import com.google.idea.blaze.android.sdk.BlazeSdkProvider;
 import com.google.idea.blaze.android.sync.model.AndroidSdkPlatform;
 import com.google.idea.blaze.android.sync.model.BlazeAndroidSyncData;
 import com.google.idea.blaze.base.async.executor.BlazeExecutor;
@@ -99,7 +99,8 @@
             }
             BlazeCommand.Builder command =
                 BlazeCommand.builder(
-                    Blaze.getBuildSystem(project), BlazeCommandName.MOBILE_INSTALL);
+                    Blaze.getBuildSystemProvider(project).getBinaryPath(),
+                    BlazeCommandName.MOBILE_INSTALL);
             command.addBlazeFlags(BlazeFlags.adbSerialFlags(device.getSerialNumber()));
 
             if (USE_SDK_ADB.getValue()) {
@@ -186,7 +187,7 @@
     if (androidSdkPlatform == null) {
       return null;
     }
-    Sdk sdk = AndroidSdkUtils.findSuitableAndroidSdk(androidSdkPlatform.androidSdk);
+    Sdk sdk = BlazeSdkProvider.getInstance().findSdk(androidSdkPlatform.androidSdk);
     if (sdk == null) {
       return null;
     }
diff --git a/aswb/src/com/google/idea/blaze/android/run/deployinfo/BlazeApkDeployInfoProtoHelper.java b/aswb/src/com/google/idea/blaze/android/run/deployinfo/BlazeApkDeployInfoProtoHelper.java
index 241fd76..ffe8c6e 100644
--- a/aswb/src/com/google/idea/blaze/android/run/deployinfo/BlazeApkDeployInfoProtoHelper.java
+++ b/aswb/src/com/google/idea/blaze/android/run/deployinfo/BlazeApkDeployInfoProtoHelper.java
@@ -91,7 +91,7 @@
         BlazeInfo.getInstance()
             .runBlazeInfo(
                 context,
-                Blaze.getBuildSystem(project),
+                Blaze.getBuildSystemProvider(project).getBinaryPath(),
                 workspaceRoot,
                 buildFlags,
                 BlazeInfo.EXECUTION_ROOT_KEY);
diff --git a/aswb/src/com/google/idea/blaze/android/run/runner/BlazeApkBuildStepNormalBuild.java b/aswb/src/com/google/idea/blaze/android/run/runner/BlazeApkBuildStepNormalBuild.java
index 835aa1d..1029f85 100644
--- a/aswb/src/com/google/idea/blaze/android/run/runner/BlazeApkBuildStepNormalBuild.java
+++ b/aswb/src/com/google/idea/blaze/android/run/runner/BlazeApkBuildStepNormalBuild.java
@@ -63,7 +63,8 @@
           @Override
           protected void execute(@NotNull BlazeContext context) {
             BlazeCommand.Builder command =
-                BlazeCommand.builder(Blaze.getBuildSystem(project), BlazeCommandName.BUILD);
+                BlazeCommand.builder(
+                    Blaze.getBuildSystemProvider(project).getBinaryPath(), BlazeCommandName.BUILD);
             WorkspaceRoot workspaceRoot = WorkspaceRoot.fromProject(project);
 
             command
diff --git a/aswb/src/com/google/idea/blaze/android/run/test/BlazeAndroidTestClassRunConfigurationProducer.java b/aswb/src/com/google/idea/blaze/android/run/test/BlazeAndroidTestClassRunConfigurationProducer.java
index a9b9515..bbafcc0 100644
--- a/aswb/src/com/google/idea/blaze/android/run/test/BlazeAndroidTestClassRunConfigurationProducer.java
+++ b/aswb/src/com/google/idea/blaze/android/run/test/BlazeAndroidTestClassRunConfigurationProducer.java
@@ -75,7 +75,7 @@
     }
     sourceElement.set(testClass);
 
-    TargetIdeInfo target = RunUtil.targetForTestClass(context.getProject(), testClass, null);
+    TargetIdeInfo target = RunUtil.targetForTestClass(testClass, null);
     if (target == null) {
       return false;
     }
diff --git a/aswb/src/com/google/idea/blaze/android/run/test/BlazeAndroidTestLaunchTask.java b/aswb/src/com/google/idea/blaze/android/run/test/BlazeAndroidTestLaunchTask.java
index 7283e53..5bd9892 100644
--- a/aswb/src/com/google/idea/blaze/android/run/test/BlazeAndroidTestLaunchTask.java
+++ b/aswb/src/com/google/idea/blaze/android/run/test/BlazeAndroidTestLaunchTask.java
@@ -132,7 +132,8 @@
 
                         BlazeCommand.Builder commandBuilder =
                             BlazeCommand.builder(
-                                    Blaze.getBuildSystem(project), BlazeCommandName.TEST)
+                                    Blaze.getBuildSystemProvider(project).getBinaryPath(),
+                                    BlazeCommandName.TEST)
                                 .addTargets(target);
                         // Build flags must match BlazeBeforeRunTask.
                         commandBuilder.addBlazeFlags(buildFlags);
diff --git a/aswb/src/com/google/idea/blaze/android/run/test/BlazeAndroidTestMethodRunConfigurationProducer.java b/aswb/src/com/google/idea/blaze/android/run/test/BlazeAndroidTestMethodRunConfigurationProducer.java
index a0d4878..d3cbc78 100644
--- a/aswb/src/com/google/idea/blaze/android/run/test/BlazeAndroidTestMethodRunConfigurationProducer.java
+++ b/aswb/src/com/google/idea/blaze/android/run/test/BlazeAndroidTestMethodRunConfigurationProducer.java
@@ -74,7 +74,7 @@
       return false;
     }
 
-    TargetIdeInfo target = RunUtil.targetForTestClass(context.getProject(), containingClass, null);
+    TargetIdeInfo target = RunUtil.targetForTestClass(containingClass, null);
     if (target == null) {
       return false;
     }
diff --git a/aswb/src/com/google/idea/blaze/android/run/test/BlazeAndroidTestRunContext.java b/aswb/src/com/google/idea/blaze/android/run/test/BlazeAndroidTestRunContext.java
index 7f5fb95..fd99489 100644
--- a/aswb/src/com/google/idea/blaze/android/run/test/BlazeAndroidTestRunContext.java
+++ b/aswb/src/com/google/idea/blaze/android/run/test/BlazeAndroidTestRunContext.java
@@ -45,7 +45,6 @@
 import com.google.idea.blaze.base.model.primitives.Label;
 import com.google.idea.blaze.base.run.BlazeCommandRunConfiguration;
 import com.google.idea.blaze.base.run.smrunner.BlazeTestEventsHandler;
-import com.google.idea.common.experiments.BoolExperiment;
 import com.intellij.execution.ExecutionException;
 import com.intellij.execution.Executor;
 import com.intellij.execution.executors.DefaultDebugExecutor;
@@ -61,9 +60,6 @@
 /** Run context for android_test. */
 class BlazeAndroidTestRunContext implements BlazeAndroidRunContext {
 
-  private static final BoolExperiment smRunnerUiEnabled =
-      new BoolExperiment("use.smrunner.ui.android", true);
-
   private final Project project;
   private final AndroidFacet facet;
   private final BlazeCommandRunConfiguration runConfiguration;
@@ -97,7 +93,7 @@
     this.apkProvider = new BlazeApkProvider(project, buildStep.getDeployInfo());
 
     BlazeTestEventsHandler testEventsHandler = null;
-    if (smRunnerUiEnabled.getValue() && !isDebugging(env.getExecutor())) {
+    if (!isDebugging(env.getExecutor())) {
       testEventsHandler =
           BlazeTestEventsHandler.getHandlerForTarget(project, runConfiguration.getTarget());
       assert (testEventsHandler != null);
diff --git a/aswb/src/com/google/idea/blaze/android/sdk/BlazeSdkProvider.java b/aswb/src/com/google/idea/blaze/android/sdk/BlazeSdkProvider.java
new file mode 100644
index 0000000..2222bd4
--- /dev/null
+++ b/aswb/src/com/google/idea/blaze/android/sdk/BlazeSdkProvider.java
@@ -0,0 +1,35 @@
+/*
+ * Copyright 2017 The Bazel Authors. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.idea.blaze.android.sdk;
+
+import com.intellij.openapi.components.ServiceManager;
+import com.intellij.openapi.projectRoots.Sdk;
+import java.util.List;
+import javax.annotation.Nullable;
+
+/** Indirection to Sdks for testing purposes. */
+public interface BlazeSdkProvider {
+  static BlazeSdkProvider getInstance() {
+    return ServiceManager.getService(BlazeSdkProvider.class);
+  }
+
+  List<Sdk> getAllAndroidSdks();
+
+  Sdk findSdk(String targetHash);
+
+  @Nullable
+  String getSdkTargetHash(Sdk sdk);
+}
diff --git a/aswb/src/com/google/idea/blaze/android/sdk/BlazeSdkProviderImpl.java b/aswb/src/com/google/idea/blaze/android/sdk/BlazeSdkProviderImpl.java
new file mode 100644
index 0000000..d4c81ee
--- /dev/null
+++ b/aswb/src/com/google/idea/blaze/android/sdk/BlazeSdkProviderImpl.java
@@ -0,0 +1,45 @@
+/*
+ * Copyright 2017 The Bazel Authors. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.idea.blaze.android.sdk;
+
+import com.google.idea.blaze.android.compatibility.Compatibility.AndroidSdkUtils;
+import com.intellij.openapi.projectRoots.Sdk;
+import java.util.List;
+import javax.annotation.Nullable;
+import org.jetbrains.android.sdk.AndroidSdkAdditionalData;
+
+/** Indirection to Sdks for testing purposes. */
+public class BlazeSdkProviderImpl implements BlazeSdkProvider {
+  @Override
+  public List<Sdk> getAllAndroidSdks() {
+    return AndroidSdkUtils.getAllAndroidSdks();
+  }
+
+  @Override
+  public Sdk findSdk(String targetHash) {
+    return AndroidSdkUtils.findSuitableAndroidSdk(targetHash);
+  }
+
+  @Override
+  @Nullable
+  public String getSdkTargetHash(Sdk sdk) {
+    AndroidSdkAdditionalData additionalData = AndroidSdkUtils.getAndroidSdkAdditionalData(sdk);
+    if (additionalData == null) {
+      return null;
+    }
+    return additionalData.getBuildTargetHashString();
+  }
+}
diff --git a/aswb/src/com/google/idea/blaze/android/sdk/MockBlazeSdkProvider.java b/aswb/src/com/google/idea/blaze/android/sdk/MockBlazeSdkProvider.java
new file mode 100644
index 0000000..2a97716
--- /dev/null
+++ b/aswb/src/com/google/idea/blaze/android/sdk/MockBlazeSdkProvider.java
@@ -0,0 +1,54 @@
+/*
+ * Copyright 2017 The Bazel Authors. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.idea.blaze.android.sdk;
+
+import com.google.common.collect.Maps;
+import com.intellij.openapi.projectRoots.Sdk;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.Map.Entry;
+import javax.annotation.Nullable;
+
+/** Indirection to Sdks for testing purposes. */
+public class MockBlazeSdkProvider implements BlazeSdkProvider {
+  Map<String, Sdk> sdks = Maps.newHashMap();
+
+  public void addSdk(String targetHash, Sdk sdk) {
+    sdks.put(targetHash, sdk);
+  }
+
+  @Override
+  public List<Sdk> getAllAndroidSdks() {
+    return new ArrayList<>(sdks.values());
+  }
+
+  @Override
+  public Sdk findSdk(String targetHash) {
+    return sdks.get(targetHash);
+  }
+
+  @Override
+  @Nullable
+  public String getSdkTargetHash(Sdk sdk) {
+    return sdks.entrySet()
+        .stream()
+        .filter(entry -> entry.getValue() == sdk)
+        .map(Entry::getKey)
+        .findFirst()
+        .orElse(null);
+  }
+}
diff --git a/aswb/src/com/google/idea/blaze/android/sync/BlazeAndroidSyncPlugin.java b/aswb/src/com/google/idea/blaze/android/sync/BlazeAndroidSyncPlugin.java
index bee4d9a..07db0d0 100644
--- a/aswb/src/com/google/idea/blaze/android/sync/BlazeAndroidSyncPlugin.java
+++ b/aswb/src/com/google/idea/blaze/android/sync/BlazeAndroidSyncPlugin.java
@@ -17,12 +17,12 @@
 
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableSet;
-import com.google.idea.blaze.android.compatibility.Compatibility.AndroidSdkUtils;
 import com.google.idea.blaze.android.compatibility.Compatibility.IdeSdks;
 import com.google.idea.blaze.android.cppapi.NdkSupport;
 import com.google.idea.blaze.android.projectview.AndroidMinSdkSection;
 import com.google.idea.blaze.android.projectview.AndroidSdkPlatformSection;
 import com.google.idea.blaze.android.projectview.GeneratedAndroidResourcesSection;
+import com.google.idea.blaze.android.sdk.BlazeSdkProvider;
 import com.google.idea.blaze.android.sync.importer.BlazeAndroidWorkspaceImporter;
 import com.google.idea.blaze.android.sync.model.AndroidSdkPlatform;
 import com.google.idea.blaze.android.sync.model.BlazeAndroidImportResult;
@@ -111,6 +111,10 @@
 
   @Override
   public void installSdks(BlazeContext context) {
+    if (ApplicationManager.getApplication().isUnitTestMode()) {
+      return;
+    }
+
     File path = IdeSdks.getAndroidSdkPath();
     if (path != null) {
       context.output(new StatusOutput("Installing SDK platforms..."));
@@ -176,7 +180,7 @@
     if (androidSdkPlatform == null) {
       return;
     }
-    Sdk sdk = AndroidSdkUtils.findSuitableAndroidSdk(androidSdkPlatform.androidSdk);
+    Sdk sdk = BlazeSdkProvider.getInstance().findSdk(androidSdkPlatform.androidSdk);
     if (sdk == null) {
       IssueOutput.error(
               String.format("Android platform '%s' not found.", androidSdkPlatform.androidSdk))
diff --git a/aswb/src/com/google/idea/blaze/android/sync/sdk/AndroidSdkFromProjectView.java b/aswb/src/com/google/idea/blaze/android/sync/sdk/AndroidSdkFromProjectView.java
index b47e238..c4eb537 100644
--- a/aswb/src/com/google/idea/blaze/android/sync/sdk/AndroidSdkFromProjectView.java
+++ b/aswb/src/com/google/idea/blaze/android/sync/sdk/AndroidSdkFromProjectView.java
@@ -18,9 +18,9 @@
 import com.google.common.base.Joiner;
 import com.google.common.collect.Lists;
 import com.google.common.collect.Sets;
-import com.google.idea.blaze.android.compatibility.Compatibility.AndroidSdkUtils;
 import com.google.idea.blaze.android.projectview.AndroidMinSdkSection;
 import com.google.idea.blaze.android.projectview.AndroidSdkPlatformSection;
+import com.google.idea.blaze.android.sdk.BlazeSdkProvider;
 import com.google.idea.blaze.android.sync.model.AndroidSdkPlatform;
 import com.google.idea.blaze.base.projectview.ProjectViewSet;
 import com.google.idea.blaze.base.projectview.ProjectViewSet.ProjectViewFile;
@@ -40,7 +40,7 @@
   @Nullable
   public static AndroidSdkPlatform getAndroidSdkPlatform(
       BlazeContext context, ProjectViewSet projectViewSet) {
-    Collection<Sdk> sdks = AndroidSdkUtils.getAllAndroidSdks();
+    List<Sdk> sdks = BlazeSdkProvider.getInstance().getAllAndroidSdks();
     if (sdks.isEmpty()) {
       IssueOutput.error("No Android SDK configured. Please use the SDK manager to configure.")
           .navigatable(
@@ -81,7 +81,7 @@
       return null;
     }
 
-    Sdk sdk = AndroidSdkUtils.findSuitableAndroidSdk(androidSdk);
+    Sdk sdk = BlazeSdkProvider.getInstance().findSdk(androidSdk);
     if (sdk == null) {
       ProjectViewFile projectViewFile = projectViewSet.getTopLevelProjectViewFile();
       IssueOutput.error(
@@ -104,19 +104,10 @@
     return new AndroidSdkPlatform(androidSdk, androidMinSdk);
   }
 
-  @Nullable
-  public static String getSdkTargetHash(Sdk sdk) {
-    AndroidSdkAdditionalData additionalData = AndroidSdkUtils.getAndroidSdkAdditionalData(sdk);
-    if (additionalData == null) {
-      return null;
-    }
-    return additionalData.getBuildTargetHashString();
-  }
-
   public static List<String> getAvailableSdkTargetHashes(Collection<Sdk> sdks) {
     Set<String> names = Sets.newHashSet();
     for (Sdk sdk : sdks) {
-      String targetHash = getSdkTargetHash(sdk);
+      String targetHash = BlazeSdkProvider.getInstance().getSdkTargetHash(sdk);
       if (targetHash != null) {
         names.add(targetHash);
       }
diff --git a/aswb/src/com/google/idea/blaze/android/sync/sdk/SdkUtil.java b/aswb/src/com/google/idea/blaze/android/sync/sdk/SdkUtil.java
index fcbe1f9..885b931 100644
--- a/aswb/src/com/google/idea/blaze/android/sync/sdk/SdkUtil.java
+++ b/aswb/src/com/google/idea/blaze/android/sync/sdk/SdkUtil.java
@@ -16,7 +16,7 @@
 package com.google.idea.blaze.android.sync.sdk;
 
 import com.android.tools.idea.updater.configure.SdkUpdaterConfigurableProvider;
-import com.google.idea.blaze.android.compatibility.Compatibility.AndroidSdkUtils;
+import com.google.idea.blaze.android.sdk.BlazeSdkProvider;
 import com.google.idea.blaze.android.sync.model.AndroidSdkPlatform;
 import com.google.idea.blaze.android.sync.model.BlazeAndroidSyncData;
 import com.google.idea.blaze.base.model.BlazeProjectData;
@@ -49,7 +49,7 @@
     if (androidSdkPlatform == null) {
       return null;
     }
-    Sdk sdk = AndroidSdkUtils.findSuitableAndroidSdk(androidSdkPlatform.androidSdk);
+    Sdk sdk = BlazeSdkProvider.getInstance().findSdk(androidSdkPlatform.androidSdk);
     if (sdk == null) {
       return null;
     }
diff --git a/aswb/tests/integrationtests/com/google/idea/blaze/android/run/BlazeAndroidRunConfigurationCommonStateMigrationTest.java b/aswb/tests/integrationtests/com/google/idea/blaze/android/run/BlazeAndroidRunConfigurationCommonStateMigrationTest.java
index 7eca0c6..50786f9 100644
--- a/aswb/tests/integrationtests/com/google/idea/blaze/android/run/BlazeAndroidRunConfigurationCommonStateMigrationTest.java
+++ b/aswb/tests/integrationtests/com/google/idea/blaze/android/run/BlazeAndroidRunConfigurationCommonStateMigrationTest.java
@@ -18,14 +18,18 @@
 
 import static com.google.common.truth.Truth.assertThat;
 
-import com.google.idea.blaze.android.BlazeAndroidIntegrationTestCase;
+import com.google.idea.blaze.android.AndroidIntegrationTestCleanupHelper;
+import com.google.idea.blaze.android.AndroidIntegrationTestSetupRule;
+import com.google.idea.blaze.base.BlazeIntegrationTestCase;
 import java.io.StringReader;
 import java.util.List;
 import org.jdom.Element;
 import org.jdom.input.SAXBuilder;
 import org.jdom.output.Format;
 import org.jdom.output.XMLOutputter;
+import org.junit.After;
 import org.junit.Before;
+import org.junit.Rule;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.junit.runners.JUnit4;
@@ -36,8 +40,7 @@
  * migration code in BlazeAndroidRunConfigurationCommonState is removed.
  */
 @RunWith(JUnit4.class)
-public class BlazeAndroidRunConfigurationCommonStateMigrationTest
-    extends BlazeAndroidIntegrationTestCase {
+public class BlazeAndroidRunConfigurationCommonStateMigrationTest extends BlazeIntegrationTestCase {
 
   private static final String DEPLOY_TARGET_STATES_RAW_XML =
       "<android-deploy-target-states>"
@@ -71,6 +74,9 @@
           + "  <option name=\"WORKING_DIR\" value=\"/some/other/directory\" />"
           + "  <option name=\"TARGET_LOGGING_CHANNELS\" value=\"some other channels\" />"
           + "</BlazeAuto>";
+  @Rule
+  public final AndroidIntegrationTestSetupRule androidSetupRule =
+      new AndroidIntegrationTestSetupRule();
 
   private BlazeAndroidRunConfigurationCommonState state;
   private SAXBuilder saxBuilder;
@@ -83,6 +89,11 @@
     xmlOutputter = new XMLOutputter(Format.getCompactFormat());
   }
 
+  @After
+  public final void doTeardown() {
+    AndroidIntegrationTestCleanupHelper.cleanUp(getProject());
+  }
+
   private String formatRawXml(String rawXml) throws Exception {
     Element element =
         saxBuilder.build(new StringReader("<?xml version=\"1.0\"?>" + rawXml)).getRootElement();
diff --git a/aswb/tests/integrationtests/com/google/idea/blaze/android/run/BlazeAndroidRunConfigurationCommonStateTest.java b/aswb/tests/integrationtests/com/google/idea/blaze/android/run/BlazeAndroidRunConfigurationCommonStateTest.java
index 1964580..8844355 100644
--- a/aswb/tests/integrationtests/com/google/idea/blaze/android/run/BlazeAndroidRunConfigurationCommonStateTest.java
+++ b/aswb/tests/integrationtests/com/google/idea/blaze/android/run/BlazeAndroidRunConfigurationCommonStateTest.java
@@ -19,8 +19,10 @@
 import static com.google.common.truth.Truth.assertThat;
 
 import com.google.common.collect.ImmutableList;
-import com.google.idea.blaze.android.BlazeAndroidIntegrationTestCase;
+import com.google.idea.blaze.android.AndroidIntegrationTestCleanupHelper;
+import com.google.idea.blaze.android.AndroidIntegrationTestSetupRule;
 import com.google.idea.blaze.android.cppapi.NdkSupport;
+import com.google.idea.blaze.base.BlazeIntegrationTestCase;
 import com.google.idea.common.experiments.ExperimentService;
 import com.google.idea.common.experiments.MockExperimentService;
 import com.intellij.openapi.util.InvalidDataException;
@@ -28,15 +30,20 @@
 import org.jdom.Element;
 import org.jdom.output.Format;
 import org.jdom.output.XMLOutputter;
+import org.junit.After;
 import org.junit.Before;
+import org.junit.Rule;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.junit.runners.JUnit4;
 
 /** Tests for {@link BlazeAndroidRunConfigurationCommonState}. */
 @RunWith(JUnit4.class)
-public class BlazeAndroidRunConfigurationCommonStateTest extends BlazeAndroidIntegrationTestCase {
+public class BlazeAndroidRunConfigurationCommonStateTest extends BlazeIntegrationTestCase {
 
+  @Rule
+  public final AndroidIntegrationTestSetupRule androidSetupRule =
+      new AndroidIntegrationTestSetupRule();
   private BlazeAndroidRunConfigurationCommonState state;
 
   @Before
@@ -50,6 +57,11 @@
     state = new BlazeAndroidRunConfigurationCommonState(buildSystem().getName(), false);
   }
 
+  @After
+  public final void doTeardown() {
+    AndroidIntegrationTestCleanupHelper.cleanUp(getProject());
+  }
+
   @Test
   public void readAndWriteShouldMatch() throws InvalidDataException, WriteExternalException {
     state.setUserFlags(ImmutableList.of("--flag1", "--flag2"));
diff --git a/aswb/tests/integrationtests/com/google/idea/blaze/android/run/binary/BlazeAndroidBinaryRunConfigurationStateTest.java b/aswb/tests/integrationtests/com/google/idea/blaze/android/run/binary/BlazeAndroidBinaryRunConfigurationStateTest.java
index 05de9a5..1e55ac2 100644
--- a/aswb/tests/integrationtests/com/google/idea/blaze/android/run/binary/BlazeAndroidBinaryRunConfigurationStateTest.java
+++ b/aswb/tests/integrationtests/com/google/idea/blaze/android/run/binary/BlazeAndroidBinaryRunConfigurationStateTest.java
@@ -19,9 +19,11 @@
 import static com.google.common.truth.Truth.assertThat;
 
 import com.google.common.collect.ImmutableList;
-import com.google.idea.blaze.android.BlazeAndroidIntegrationTestCase;
+import com.google.idea.blaze.android.AndroidIntegrationTestCleanupHelper;
+import com.google.idea.blaze.android.AndroidIntegrationTestSetupRule;
 import com.google.idea.blaze.android.cppapi.NdkSupport;
 import com.google.idea.blaze.android.run.BlazeAndroidRunConfigurationCommonState;
+import com.google.idea.blaze.base.BlazeIntegrationTestCase;
 import com.google.idea.blaze.base.run.state.RunConfigurationStateEditor;
 import com.google.idea.common.experiments.ExperimentService;
 import com.google.idea.common.experiments.MockExperimentService;
@@ -31,15 +33,20 @@
 import org.jdom.Element;
 import org.jdom.output.Format;
 import org.jdom.output.XMLOutputter;
+import org.junit.After;
 import org.junit.Before;
+import org.junit.Rule;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.junit.runners.JUnit4;
 
 /** Tests for {@link BlazeAndroidBinaryRunConfigurationState}. */
 @RunWith(JUnit4.class)
-public class BlazeAndroidBinaryRunConfigurationStateTest extends BlazeAndroidIntegrationTestCase {
+public class BlazeAndroidBinaryRunConfigurationStateTest extends BlazeIntegrationTestCase {
 
+  @Rule
+  public final AndroidIntegrationTestSetupRule androidSetupRule =
+      new AndroidIntegrationTestSetupRule();
   private BlazeAndroidBinaryRunConfigurationState state;
 
   @Before
@@ -53,6 +60,11 @@
     state = new BlazeAndroidBinaryRunConfigurationState(buildSystem().getName());
   }
 
+  @After
+  public final void doTeardown() {
+    AndroidIntegrationTestCleanupHelper.cleanUp(getProject());
+  }
+
   @Test
   public void readAndWriteShouldMatch() throws InvalidDataException, WriteExternalException {
     BlazeAndroidRunConfigurationCommonState commonState = state.getCommonState();
diff --git a/aswb/tests/integrationtests/com/google/idea/blaze/android/run/test/BlazeAndroidTestRunConfigurationStateTest.java b/aswb/tests/integrationtests/com/google/idea/blaze/android/run/test/BlazeAndroidTestRunConfigurationStateTest.java
index f04a14e..1085d2b 100644
--- a/aswb/tests/integrationtests/com/google/idea/blaze/android/run/test/BlazeAndroidTestRunConfigurationStateTest.java
+++ b/aswb/tests/integrationtests/com/google/idea/blaze/android/run/test/BlazeAndroidTestRunConfigurationStateTest.java
@@ -18,9 +18,11 @@
 import static com.google.common.truth.Truth.assertThat;
 
 import com.google.common.collect.ImmutableList;
-import com.google.idea.blaze.android.BlazeAndroidIntegrationTestCase;
+import com.google.idea.blaze.android.AndroidIntegrationTestCleanupHelper;
+import com.google.idea.blaze.android.AndroidIntegrationTestSetupRule;
 import com.google.idea.blaze.android.cppapi.NdkSupport;
 import com.google.idea.blaze.android.run.BlazeAndroidRunConfigurationCommonState;
+import com.google.idea.blaze.base.BlazeIntegrationTestCase;
 import com.google.idea.blaze.base.run.state.RunConfigurationStateEditor;
 import com.google.idea.common.experiments.ExperimentService;
 import com.google.idea.common.experiments.MockExperimentService;
@@ -30,15 +32,20 @@
 import org.jdom.Element;
 import org.jdom.output.Format;
 import org.jdom.output.XMLOutputter;
+import org.junit.After;
 import org.junit.Before;
+import org.junit.Rule;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.junit.runners.JUnit4;
 
 /** Tests for {@link BlazeAndroidTestRunConfigurationState}. */
 @RunWith(JUnit4.class)
-public class BlazeAndroidTestRunConfigurationStateTest extends BlazeAndroidIntegrationTestCase {
+public class BlazeAndroidTestRunConfigurationStateTest extends BlazeIntegrationTestCase {
 
+  @Rule
+  public final AndroidIntegrationTestSetupRule androidSetupRule =
+      new AndroidIntegrationTestSetupRule();
   private BlazeAndroidTestRunConfigurationState state;
 
   @Before
@@ -52,6 +59,11 @@
     state = new BlazeAndroidTestRunConfigurationState(buildSystem().getName());
   }
 
+  @After
+  public final void doTeardown() {
+    AndroidIntegrationTestCleanupHelper.cleanUp(getProject());
+  }
+
   @Test
   public void readAndWriteShouldMatch() throws InvalidDataException, WriteExternalException {
     BlazeAndroidRunConfigurationCommonState commonState = state.getCommonState();
diff --git a/aswb/tests/integrationtests/com/google/idea/blaze/android/run/test/smrunner/BlazeAndroidTestEventsHandlerTest.java b/aswb/tests/integrationtests/com/google/idea/blaze/android/run/test/smrunner/BlazeAndroidTestEventsHandlerTest.java
index 474e1b4..2a4bf02 100644
--- a/aswb/tests/integrationtests/com/google/idea/blaze/android/run/test/smrunner/BlazeAndroidTestEventsHandlerTest.java
+++ b/aswb/tests/integrationtests/com/google/idea/blaze/android/run/test/smrunner/BlazeAndroidTestEventsHandlerTest.java
@@ -18,7 +18,9 @@
 import static com.google.common.truth.Truth.assertThat;
 
 import com.google.common.collect.Iterables;
-import com.google.idea.blaze.android.BlazeAndroidIntegrationTestCase;
+import com.google.idea.blaze.android.AndroidIntegrationTestCleanupHelper;
+import com.google.idea.blaze.android.AndroidIntegrationTestSetupRule;
+import com.google.idea.blaze.base.BlazeIntegrationTestCase;
 import com.google.idea.blaze.base.model.primitives.WorkspacePath;
 import com.intellij.execution.Location;
 import com.intellij.openapi.vfs.VirtualFileManager;
@@ -28,15 +30,20 @@
 import com.intellij.psi.PsiMethod;
 import com.intellij.psi.search.GlobalSearchScope;
 import javax.annotation.Nullable;
+import org.junit.After;
 import org.junit.Before;
+import org.junit.Rule;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.junit.runners.JUnit4;
 
 /** Integration tests for {@link BlazeAndroidTestEventsHandler}. */
 @RunWith(JUnit4.class)
-public class BlazeAndroidTestEventsHandlerTest extends BlazeAndroidIntegrationTestCase {
+public class BlazeAndroidTestEventsHandlerTest extends BlazeIntegrationTestCase {
 
+  @Rule
+  public final AndroidIntegrationTestSetupRule androidSetupRule =
+      new AndroidIntegrationTestSetupRule();
   private final BlazeAndroidTestEventsHandler handler = new BlazeAndroidTestEventsHandler();
 
   @Before
@@ -56,6 +63,11 @@
         "public class JUnit4 {}");
   }
 
+  @After
+  public final void doTeardown() {
+    AndroidIntegrationTestCleanupHelper.cleanUp(getProject());
+  }
+
   @Test
   public void testSuiteLocationResolves() {
     PsiFile javaFile =
diff --git a/aswb/tests/integrationtests/com/google/idea/blaze/android/sync/AndroidSyncTest.java b/aswb/tests/integrationtests/com/google/idea/blaze/android/sync/AndroidSyncTest.java
new file mode 100644
index 0000000..6e5a68d
--- /dev/null
+++ b/aswb/tests/integrationtests/com/google/idea/blaze/android/sync/AndroidSyncTest.java
@@ -0,0 +1,148 @@
+/*
+ * Copyright 2017 The Bazel Authors. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.idea.blaze.android.sync;
+
+import static com.google.common.truth.Truth.assertThat;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+import com.google.common.collect.ImmutableList;
+import com.google.idea.blaze.android.AndroidIntegrationTestCleanupHelper;
+import com.google.idea.blaze.android.AndroidIntegrationTestSetupRule;
+import com.google.idea.blaze.android.sdk.BlazeSdkProvider;
+import com.google.idea.blaze.android.sdk.MockBlazeSdkProvider;
+import com.google.idea.blaze.base.ideinfo.AndroidIdeInfo;
+import com.google.idea.blaze.base.ideinfo.TargetIdeInfo;
+import com.google.idea.blaze.base.ideinfo.TargetMap;
+import com.google.idea.blaze.base.ideinfo.TargetMapBuilder;
+import com.google.idea.blaze.base.model.BlazeProjectData;
+import com.google.idea.blaze.base.model.primitives.WorkspacePath;
+import com.google.idea.blaze.base.model.primitives.WorkspaceType;
+import com.google.idea.blaze.base.sync.BlazeSyncIntegrationTestCase;
+import com.google.idea.blaze.base.sync.BlazeSyncParams;
+import com.google.idea.blaze.base.sync.BlazeSyncParams.SyncMode;
+import com.google.idea.blaze.base.sync.data.BlazeDataStorage;
+import com.google.idea.blaze.base.sync.data.BlazeProjectDataManager;
+import com.intellij.openapi.module.Module;
+import com.intellij.openapi.projectRoots.Sdk;
+import com.intellij.openapi.projectRoots.SdkTypeId;
+import com.intellij.openapi.roots.ContentEntry;
+import com.intellij.openapi.vfs.VirtualFile;
+import org.jetbrains.android.facet.AndroidFacet;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** Android-specific sync integration tests. */
+@RunWith(JUnit4.class)
+public class AndroidSyncTest extends BlazeSyncIntegrationTestCase {
+
+  @Rule
+  public final AndroidIntegrationTestSetupRule androidSetupRule =
+      new AndroidIntegrationTestSetupRule();
+
+  @Before
+  public void setup() {
+    mockSdk("android-25", "Android 25 SDK");
+  }
+
+  @After
+  public final void doTeardown() {
+    AndroidIntegrationTestCleanupHelper.cleanUp(getProject());
+  }
+
+  private void mockSdk(String targetHash, String sdkName) {
+    SdkTypeId sdkType = mock(SdkTypeId.class);
+    when(sdkType.getName()).thenReturn("Android SDK");
+    Sdk sdk = mock(Sdk.class);
+    when(sdk.getName()).thenReturn(sdkName);
+    when(sdk.getSdkType()).thenReturn(sdkType);
+    MockBlazeSdkProvider sdkProvider = (MockBlazeSdkProvider) BlazeSdkProvider.getInstance();
+    sdkProvider.addSdk(targetHash, sdk);
+  }
+
+  @Test
+  public void testSimpleSync() throws Exception {
+    setProjectView(
+        "directories:",
+        "  java/com/google",
+        "targets:",
+        "  //java/com/google:lib",
+        "android_sdk_platform: android-25");
+
+    workspace.createFile(
+        new WorkspacePath("java/com/google/Source.java"),
+        "package com.google;",
+        "public class Source {}");
+
+    workspace.createFile(
+        new WorkspacePath("java/com/google/Other.java"),
+        "package com.google;",
+        "public class Other {}");
+
+    VirtualFile javaRoot = workspace.createDirectory(new WorkspacePath("java/com/google"));
+
+    TargetMap targetMap =
+        TargetMapBuilder.builder()
+            .addTarget(
+                TargetIdeInfo.builder()
+                    .setBuildFile(sourceRoot("java/com/google/BUILD"))
+                    .setLabel("//java/com/google:lib")
+                    .setKind("android_library")
+                    .setAndroidInfo(
+                        AndroidIdeInfo.builder()
+                            .setManifestFile(sourceRoot("java/com/google/AndroidManifest.xml"))
+                            .addResource(sourceRoot("java/com/google/res/values/strings.xml"))
+                            .setGenerateResourceClass(true))
+                    .addSource(sourceRoot("java/com/google/Source.java"))
+                    .addSource(sourceRoot("java/com/google/Other.java")))
+            .build();
+
+    setTargetMap(targetMap);
+
+    runBlazeSync(
+        new BlazeSyncParams.Builder("Sync", SyncMode.INCREMENTAL)
+            .addProjectViewTargets(true)
+            .build());
+
+    errorCollector.assertNoIssues();
+
+    BlazeProjectData blazeProjectData =
+        BlazeProjectDataManager.getInstance(getProject()).getBlazeProjectData();
+    assertThat(blazeProjectData).isNotNull();
+    assertThat(blazeProjectData.targetMap).isEqualTo(targetMap);
+    assertThat(blazeProjectData.workspaceLanguageSettings.getWorkspaceType())
+        .isEqualTo(WorkspaceType.ANDROID);
+
+    ImmutableList<ContentEntry> contentEntries = getWorkspaceContentEntries();
+    assertThat(contentEntries).hasSize(1);
+    assertThat(findContentEntry(javaRoot)).isNotNull();
+    assertThat(findContentEntry(javaRoot).getSourceFolders()).hasLength(1);
+
+    // Check that the workspace is set to android
+    Module workspaceModule = getModuleCreatedDuringSync(BlazeDataStorage.WORKSPACE_MODULE_NAME);
+    assertThat(workspaceModule).isNotNull();
+    assertThat(AndroidFacet.getInstance(workspaceModule)).isNotNull();
+
+    // Check that a resource module was created
+    Module resourceModule = getModuleCreatedDuringSync("java.com.google.lib");
+    assertThat(resourceModule).isNotNull();
+    assertThat(AndroidFacet.getInstance(resourceModule)).isNotNull();
+  }
+}
diff --git a/aswb/tests/unittests/com/google/idea/blaze/android/sync/importer/BlazeAndroidWorkspaceImporterTest.java b/aswb/tests/unittests/com/google/idea/blaze/android/sync/importer/BlazeAndroidWorkspaceImporterTest.java
index c839293..2301f3a 100644
--- a/aswb/tests/unittests/com/google/idea/blaze/android/sync/importer/BlazeAndroidWorkspaceImporterTest.java
+++ b/aswb/tests/unittests/com/google/idea/blaze/android/sync/importer/BlazeAndroidWorkspaceImporterTest.java
@@ -95,8 +95,7 @@
     BlazeExecutor blazeExecutor = new MockBlazeExecutor();
     applicationServices.register(BlazeExecutor.class, blazeExecutor);
 
-    projectServices.register(
-        BlazeImportSettingsManager.class, new BlazeImportSettingsManager(project));
+    projectServices.register(BlazeImportSettingsManager.class, new BlazeImportSettingsManager());
     BlazeImportSettingsManager.getInstance(getProject()).setImportSettings(DUMMY_IMPORT_SETTINGS);
 
     MockFileAttributeProvider mockFileAttributeProvider = new MockFileAttributeProvider();
diff --git a/base/BUILD b/base/BUILD
index 850f1dd..7afe714 100644
--- a/base/BUILD
+++ b/base/BUILD
@@ -11,7 +11,7 @@
         "//common/experiments",
         "//common/formatter",
         "//intellij_platform_sdk:plugin_api",
-        "//proto_deps",
+        "//proto:proto_deps",
         "//sdkcompat",
         "@jsr305_annotations//jar",
     ],
@@ -46,7 +46,7 @@
         ":unit_test_utils",
         "//base",
         "//intellij_platform_sdk:plugin_api_for_tests",
-        "//proto_deps",
+        "//proto:proto_deps",
         "//testing:lib",
         "@jsr305_annotations//jar",
         "@junit//jar",
@@ -91,7 +91,7 @@
         "//common/experiments",
         "//common/experiments:unit_test_utils",
         "//intellij_platform_sdk:plugin_api_for_tests",
-        "//proto_deps",
+        "//proto:proto_deps",
         "@jsr305_annotations//jar",
         "@junit//jar",
     ],
@@ -110,7 +110,7 @@
         ":integration_test_utils",
         ":unit_test_utils",
         "//intellij_platform_sdk:plugin_api_for_tests",
-        "//proto_deps",
+        "//proto:proto_deps",
         "@jsr305_annotations//jar",
         "@junit//jar",
     ],
diff --git a/base/src/META-INF/blaze-base.xml b/base/src/META-INF/blaze-base.xml
index 913bdb5..781e35a 100644
--- a/base/src/META-INF/blaze-base.xml
+++ b/base/src/META-INF/blaze-base.xml
@@ -183,7 +183,6 @@
     <projectService serviceInterface="com.google.idea.blaze.base.targetmaps.TransitiveDependencyMap"
                     serviceImplementation="com.google.idea.blaze.base.targetmaps.TransitiveDependencyMap"/>
     <projectService serviceImplementation="com.google.idea.blaze.base.settings.BlazeImportSettingsManager"/>
-    <projectService serviceImplementation="com.google.idea.blaze.base.settings.BlazeImportSettingsManagerLegacy"/>
     <applicationService serviceImplementation="com.google.idea.blaze.base.settings.BlazeUserSettings"/>
     <applicationService serviceInterface="com.google.idea.blaze.base.lang.buildfile.language.semantics.BuildLanguageSpecProvider"
                         serviceImplementation="com.google.idea.blaze.base.lang.buildfile.language.semantics.BuildLanguageSpecProviderImpl"/>
@@ -233,10 +232,11 @@
     <!--<annotator language="BUILD" implementationClass="com.google.idea.blaze.base.lang.buildfile.validation.LoadErrorAnnotator"/>-->
     <annotator language="BUILD" implementationClass="com.google.idea.blaze.base.lang.buildfile.validation.GlobErrorAnnotator"/>
     <annotator language="BUILD" implementationClass="com.google.idea.blaze.base.lang.buildfile.validation.BuiltInRuleAnnotator"/>
+    <annotator language="BUILD" implementationClass="com.google.idea.blaze.base.lang.buildfile.validation.LoadStatementAnnotator"/>
     <colorSettingsPage implementation="com.google.idea.blaze.base.lang.buildfile.highlighting.BuildColorsPage"/>
     <projectService serviceImplementation="com.google.idea.blaze.base.lang.buildfile.psi.util.BuildElementGenerator"/>
     <projectService serviceImplementation="com.google.idea.blaze.base.lang.buildfile.references.BuildReferenceManager"/>
-    <referencesSearch implementation="com.google.idea.blaze.base.lang.buildfile.search.BuildLabelReferenceSearcher"/>
+    <referencesSearch implementation="com.google.idea.blaze.base.lang.buildfile.search.BuildReferenceSearcher"/>
     <referencesSearch implementation="com.google.idea.blaze.base.lang.buildfile.search.GlobReferenceSearcher"/>
     <readWriteAccessDetector implementation="com.google.idea.blaze.base.lang.buildfile.findusages.BuildReadWriteAccessDetector"/>
     <elementDescriptionProvider implementation="com.google.idea.blaze.base.lang.buildfile.findusages.BuildElementDescriptionProvider"/>
@@ -340,6 +340,7 @@
     <BuildifierBinaryProvider implementation="com.google.idea.blaze.base.buildmodifier.BazelBuildifierBinaryProvider"/>
     <BlazeCommandRunConfigurationHandlerProvider implementation="com.google.idea.blaze.base.run.confighandler.BlazeCommandGenericRunConfigurationHandlerProvider" order="last"/>
     <TestTargetHeuristic implementation="com.google.idea.blaze.base.run.TargetNameHeuristic" order="first"/>
+    <TestTargetHeuristic implementation="com.google.idea.blaze.base.run.TestTargetSourcesHeuristic" order="before TestSizeHeuristic" id="TestSourcesHeuristic"/>
     <TestTargetHeuristic implementation="com.google.idea.blaze.base.run.TestSizeHeuristic" order="last" id="TestSizeHeuristic"/>
     <RunConfigurationFactory implementation="com.google.idea.blaze.base.run.BlazeBuildTargetRunConfigurationFactory" order="last"/>
     <AspectStrategyProvider implementation="com.google.idea.blaze.base.sync.aspects.strategy.AspectStrategyProviderBazel" order="last"/>
diff --git a/base/src/com/google/idea/blaze/base/async/executor/BlazeExecutor.java b/base/src/com/google/idea/blaze/base/async/executor/BlazeExecutor.java
index f9eed17..8df0ed0 100644
--- a/base/src/com/google/idea/blaze/base/async/executor/BlazeExecutor.java
+++ b/base/src/com/google/idea/blaze/base/async/executor/BlazeExecutor.java
@@ -28,8 +28,8 @@
 import com.intellij.openapi.util.Computable;
 import com.intellij.util.ui.UIUtil;
 import java.util.concurrent.Callable;
+import javax.annotation.Nullable;
 import org.jetbrains.annotations.NotNull;
-import org.jetbrains.annotations.Nullable;
 
 /** Shared thread pool for blaze tasks. */
 public abstract class BlazeExecutor {
@@ -59,7 +59,7 @@
       @NotNull final String title,
       @NotNull final Progressive progressive) {
     return submitTask(
-        project, title, true /* cancelable */, Modality.ALWAYS_BACKGROUND, progressive);
+        project, title, /* cancelable */ true, Modality.ALWAYS_BACKGROUND, progressive);
   }
 
   public static ListenableFuture<Void> submitTask(
diff --git a/base/src/com/google/idea/blaze/base/bazel/BazelBuildSystemProvider.java b/base/src/com/google/idea/blaze/base/bazel/BazelBuildSystemProvider.java
index b3d4598..efb75be 100644
--- a/base/src/com/google/idea/blaze/base/bazel/BazelBuildSystemProvider.java
+++ b/base/src/com/google/idea/blaze/base/bazel/BazelBuildSystemProvider.java
@@ -22,6 +22,7 @@
 import com.google.idea.blaze.base.model.BlazeVersionData;
 import com.google.idea.blaze.base.model.primitives.WorkspaceRoot;
 import com.google.idea.blaze.base.settings.Blaze.BuildSystem;
+import com.google.idea.blaze.base.settings.BlazeUserSettings;
 import com.intellij.openapi.fileTypes.ExactFileNameMatcher;
 import com.intellij.openapi.fileTypes.FileNameMatcher;
 import java.io.File;
@@ -35,6 +36,13 @@
     return BuildSystem.Bazel;
   }
 
+  @Nullable
+  @Override
+  public String getBinaryPath() {
+    BlazeUserSettings settings = BlazeUserSettings.getInstance();
+    return settings.getBazelBinaryPath();
+  }
+
   @Override
   public WorkspaceRootProvider getWorkspaceRootProvider() {
     return BazelWorkspaceRootProvider.INSTANCE;
@@ -65,15 +73,20 @@
   public File findBuildFileInDirectory(File directory) {
     FileAttributeProvider provider = FileAttributeProvider.getInstance();
     File child = new File(directory, "BUILD");
-    if (!provider.exists(child)) {
-      return null;
+    if (provider.exists(child)) {
+      return child;
     }
-    return child;
+    child = new File(directory, "BUILD.bazel");
+    if (provider.exists(child)) {
+      return child;
+    }
+    return null;
   }
 
   @Override
-  public FileNameMatcher buildFileMatcher() {
-    return new ExactFileNameMatcher("BUILD");
+  public ImmutableList<FileNameMatcher> buildFileMatchers() {
+    return ImmutableList.of(
+        new ExactFileNameMatcher("BUILD"), new ExactFileNameMatcher("BUILD.bazel"));
   }
 
   @Override
diff --git a/base/src/com/google/idea/blaze/base/bazel/BuildSystemProvider.java b/base/src/com/google/idea/blaze/base/bazel/BuildSystemProvider.java
index 61c7257..eb20772 100644
--- a/base/src/com/google/idea/blaze/base/bazel/BuildSystemProvider.java
+++ b/base/src/com/google/idea/blaze/base/bazel/BuildSystemProvider.java
@@ -71,6 +71,16 @@
    */
   BuildSystem buildSystem();
 
+  /** @return The location of the blaze/bazel binary. */
+  @Nullable
+  String getBinaryPath();
+
+  /** @return The location of the blaze/bazel binary to use for syncing. */
+  @Nullable
+  default String getSyncBinaryPath() {
+    return getBinaryPath();
+  }
+
   WorkspaceRootProvider getWorkspaceRootProvider();
 
   /** Directories containing artifacts produced during the build process. */
@@ -101,7 +111,7 @@
     return buildFile != null ? directory.getFileSystem().findFileByPath(buildFile.getPath()) : null;
   }
 
-  FileNameMatcher buildFileMatcher();
+  ImmutableList<FileNameMatcher> buildFileMatchers();
 
   /** Populates the passed builder with version data. */
   void populateBlazeVersionData(
diff --git a/base/src/com/google/idea/blaze/base/buildmodifier/FileSystemModifier.java b/base/src/com/google/idea/blaze/base/buildmodifier/FileSystemModifier.java
index f3586a2..96719c1 100644
--- a/base/src/com/google/idea/blaze/base/buildmodifier/FileSystemModifier.java
+++ b/base/src/com/google/idea/blaze/base/buildmodifier/FileSystemModifier.java
@@ -19,8 +19,8 @@
 import com.intellij.openapi.components.ServiceManager;
 import com.intellij.openapi.project.Project;
 import java.io.File;
+import javax.annotation.Nullable;
 import org.jetbrains.annotations.NotNull;
-import org.jetbrains.annotations.Nullable;
 
 /** Modifies the file system. Interface so we can mock it in tests */
 public abstract class FileSystemModifier {
diff --git a/base/src/com/google/idea/blaze/base/buildmodifier/FileSystemModifierImpl.java b/base/src/com/google/idea/blaze/base/buildmodifier/FileSystemModifierImpl.java
index 0d398f9..7e873ee 100644
--- a/base/src/com/google/idea/blaze/base/buildmodifier/FileSystemModifierImpl.java
+++ b/base/src/com/google/idea/blaze/base/buildmodifier/FileSystemModifierImpl.java
@@ -20,8 +20,8 @@
 import com.intellij.openapi.project.Project;
 import java.io.File;
 import java.io.IOException;
+import javax.annotation.Nullable;
 import org.jetbrains.annotations.NotNull;
-import org.jetbrains.annotations.Nullable;
 
 class FileSystemModifierImpl extends FileSystemModifier {
 
diff --git a/base/src/com/google/idea/blaze/base/command/BlazeCommand.java b/base/src/com/google/idea/blaze/base/command/BlazeCommand.java
index a337472..41e40e8 100644
--- a/base/src/com/google/idea/blaze/base/command/BlazeCommand.java
+++ b/base/src/com/google/idea/blaze/base/command/BlazeCommand.java
@@ -18,30 +18,21 @@
 import com.google.common.base.Joiner;
 import com.google.common.collect.ImmutableList;
 import com.google.idea.blaze.base.model.primitives.TargetExpression;
-import com.google.idea.blaze.base.settings.Blaze.BuildSystem;
-import com.google.idea.blaze.base.settings.BlazeUserSettings;
 import java.util.Arrays;
 import java.util.List;
-import javax.annotation.Nullable;
 import javax.annotation.concurrent.Immutable;
 
 /** A command to issue to Blaze/Bazel on the command line. */
 @Immutable
 public final class BlazeCommand {
 
-  private final BuildSystem buildSystem;
+  private final String binaryPath;
   private final BlazeCommandName name;
-  @Nullable private final String blazeBinary;
   private final ImmutableList<String> arguments;
 
-  private BlazeCommand(
-      BuildSystem buildSystem,
-      BlazeCommandName name,
-      @Nullable String blazeBinary,
-      ImmutableList<String> arguments) {
-    this.buildSystem = buildSystem;
+  private BlazeCommand(String binaryPath, BlazeCommandName name, ImmutableList<String> arguments) {
+    this.binaryPath = binaryPath;
     this.name = name;
-    this.blazeBinary = blazeBinary;
     this.arguments = arguments;
   }
 
@@ -50,49 +41,31 @@
   }
 
   public ImmutableList<String> toList() {
-    String blazeBinary = this.blazeBinary;
-    if (blazeBinary == null) {
-      blazeBinary = getBinaryPath(buildSystem);
-    }
-
     ImmutableList.Builder<String> commandLine = ImmutableList.builder();
-    commandLine.add(blazeBinary, name.toString());
+    commandLine.add(binaryPath, name.toString());
     commandLine.addAll(arguments);
     return commandLine.build();
   }
 
-  private static String getBinaryPath(BuildSystem buildSystem) {
-    BlazeUserSettings settings = BlazeUserSettings.getInstance();
-    switch (buildSystem) {
-      case Blaze:
-        return settings.getBlazeBinaryPath();
-      case Bazel:
-        return settings.getBazelBinaryPath();
-      default:
-        throw new RuntimeException("Unrecognized build system type: " + buildSystem);
-    }
-  }
-
   @Override
   public String toString() {
     return Joiner.on(' ').join(toList());
   }
 
-  public static Builder builder(BuildSystem buildSystem, BlazeCommandName name) {
-    return new Builder(buildSystem, name);
+  public static Builder builder(String binaryPath, BlazeCommandName name) {
+    return new Builder(binaryPath, name);
   }
 
   /** Builder for a blaze command */
   public static class Builder {
-    private final BuildSystem buildSystem;
+    private final String binaryPath;
     private final BlazeCommandName name;
-    @Nullable private String blazeBinary;
     private final ImmutableList.Builder<TargetExpression> targets = ImmutableList.builder();
     private final ImmutableList.Builder<String> blazeFlags = ImmutableList.builder();
     private final ImmutableList.Builder<String> exeFlags = ImmutableList.builder();
 
-    public Builder(BuildSystem buildSystem, BlazeCommandName name) {
-      this.buildSystem = buildSystem;
+    public Builder(String binaryPath, BlazeCommandName name) {
+      this.binaryPath = binaryPath;
       this.name = name;
       // Tell forge what tool we used to call blaze so we can track usage.
       addBlazeFlags(BlazeFlags.getToolTagFlag());
@@ -109,12 +82,7 @@
       }
 
       arguments.addAll(exeFlags.build());
-      return new BlazeCommand(buildSystem, name, blazeBinary, arguments.build());
-    }
-
-    public Builder setBlazeBinary(@Nullable String blazeBinary) {
-      this.blazeBinary = blazeBinary;
-      return this;
+      return new BlazeCommand(binaryPath, name, arguments.build());
     }
 
     public Builder addTargets(TargetExpression... targets) {
diff --git a/base/src/com/google/idea/blaze/base/command/ExperimentalShowArtifactsLineProcessor.java b/base/src/com/google/idea/blaze/base/command/ExperimentalShowArtifactsLineProcessor.java
index 8d512c0..9698b62 100644
--- a/base/src/com/google/idea/blaze/base/command/ExperimentalShowArtifactsLineProcessor.java
+++ b/base/src/com/google/idea/blaze/base/command/ExperimentalShowArtifactsLineProcessor.java
@@ -29,7 +29,7 @@
 
   private final List<File> fileList;
   private final Predicate<String> filter;
-  boolean insideBuildResult = false;
+  private boolean afterBuildResult = false;
 
   public ExperimentalShowArtifactsLineProcessor(List<File> fileList) {
     this(fileList, (value) -> true);
@@ -42,23 +42,17 @@
 
   @Override
   public boolean processLine(@NotNull String line) {
-    if (insideBuildResult) {
-      // Workaround for --experimental_ui: Extra newlines are inserted
-      if (line.isEmpty()) {
-        return false;
-      }
-
-      insideBuildResult = line.startsWith(OUTPUT_MARKER);
-      if (insideBuildResult) {
-        String fileName = line.substring(OUTPUT_MARKER.length());
-        if (filter.test(fileName)) {
-          fileList.add(new File(fileName));
-        }
-      }
+    if (!afterBuildResult) {
+      afterBuildResult = line.equals(OUTPUT_START);
+      return !afterBuildResult;
     }
-    if (!insideBuildResult) {
-      insideBuildResult = line.equals(OUTPUT_START);
+    if (!line.startsWith(OUTPUT_MARKER)) {
+      return true;
     }
-    return !insideBuildResult;
+    String fileName = line.substring(OUTPUT_MARKER.length());
+    if (filter.test(fileName)) {
+      fileList.add(new File(fileName));
+    }
+    return false;
   }
 }
diff --git a/base/src/com/google/idea/blaze/base/command/info/BlazeInfo.java b/base/src/com/google/idea/blaze/base/command/info/BlazeInfo.java
index 1d329aa..a6666e9 100644
--- a/base/src/com/google/idea/blaze/base/command/info/BlazeInfo.java
+++ b/base/src/com/google/idea/blaze/base/command/info/BlazeInfo.java
@@ -22,7 +22,6 @@
 import com.google.idea.blaze.base.settings.Blaze.BuildSystem;
 import com.intellij.openapi.components.ServiceManager;
 import java.util.List;
-import javax.annotation.Nullable;
 
 /** Runs the blaze info command. The results may be cached in the workspace. */
 public abstract class BlazeInfo {
@@ -77,8 +76,8 @@
    * @return The blaze info value associated with the specified key
    */
   public abstract ListenableFuture<String> runBlazeInfo(
-      @Nullable BlazeContext context,
-      BuildSystem buildSystem,
+      BlazeContext context,
+      String binaryPath,
       WorkspaceRoot workspaceRoot,
       List<String> blazeFlags,
       String key);
@@ -89,24 +88,22 @@
    * @return The blaze info value associated with the specified key
    */
   public abstract ListenableFuture<byte[]> runBlazeInfoGetBytes(
-      @Nullable BlazeContext context,
-      BuildSystem buildSystem,
+      BlazeContext context,
+      String binaryPath,
       WorkspaceRoot workspaceRoot,
       List<String> blazeFlags,
       String key);
 
   /**
    * This calls blaze info without any specific key so blaze info will return all keys and values
-   * that it has. There could be a performance cost for doing this, so the user should verify that
-   * calling this method is actually faster than calling {@link #runBlazeInfo(WorkspaceRoot, List,
-   * String)}.
+   * that it has.
    *
    * @param blazeFlags The blaze flags that will be passed to Blaze.
    * @return The blaze info data fields.
    */
   public abstract ListenableFuture<ImmutableMap<String, String>> runBlazeInfo(
-      @Nullable BlazeContext context,
-      BuildSystem buildSystem,
+      BlazeContext context,
+      String binaryPath,
       WorkspaceRoot workspaceRoot,
       List<String> blazeFlags);
 }
diff --git a/base/src/com/google/idea/blaze/base/command/info/BlazeInfoException.java b/base/src/com/google/idea/blaze/base/command/info/BlazeInfoException.java
index 39294c3..98aa7d3 100644
--- a/base/src/com/google/idea/blaze/base/command/info/BlazeInfoException.java
+++ b/base/src/com/google/idea/blaze/base/command/info/BlazeInfoException.java
@@ -17,33 +17,23 @@
 
 import javax.annotation.concurrent.Immutable;
 
-/** Exception occuring during blaze infoy */
+/** Exception occuring during blaze info */
 @Immutable
 public final class BlazeInfoException extends Exception {
   private final int exitCode;
   private final String stdout;
-  private final String stderr;
 
-  public BlazeInfoException(int exitCode, String stdout, String stderr) {
+  public BlazeInfoException(int exitCode, String stdout) {
     this.exitCode = exitCode;
     this.stdout = stdout;
-    this.stderr = stderr;
   }
 
   @Override
   public String getMessage() {
-    return "blaze info failed with exit code: " + exitCode + " and error stream: " + stderr;
-  }
-
-  public int getExitCode() {
-    return exitCode;
+    return "blaze info failed with exit code: " + exitCode;
   }
 
   public String getStdout() {
     return stdout;
   }
-
-  public String getStderr() {
-    return stderr;
-  }
 }
diff --git a/base/src/com/google/idea/blaze/base/command/info/BlazeInfoImpl.java b/base/src/com/google/idea/blaze/base/command/info/BlazeInfoImpl.java
index 4b9fe6c..3410796 100644
--- a/base/src/com/google/idea/blaze/base/command/info/BlazeInfoImpl.java
+++ b/base/src/com/google/idea/blaze/base/command/info/BlazeInfoImpl.java
@@ -19,11 +19,12 @@
 import com.google.common.util.concurrent.ListenableFuture;
 import com.google.idea.blaze.base.async.executor.BlazeExecutor;
 import com.google.idea.blaze.base.async.process.ExternalTask;
+import com.google.idea.blaze.base.async.process.LineProcessingOutputStream;
+import com.google.idea.blaze.base.async.process.PrintOutputLineProcessor;
 import com.google.idea.blaze.base.command.BlazeCommand;
 import com.google.idea.blaze.base.command.BlazeCommandName;
 import com.google.idea.blaze.base.model.primitives.WorkspaceRoot;
 import com.google.idea.blaze.base.scope.BlazeContext;
-import com.google.idea.blaze.base.settings.Blaze.BuildSystem;
 import com.intellij.openapi.diagnostic.Logger;
 import java.io.ByteArrayOutputStream;
 import java.util.List;
@@ -34,42 +35,42 @@
 
   @Override
   public ListenableFuture<String> runBlazeInfo(
-      @Nullable BlazeContext context,
-      BuildSystem buildSystem,
+      BlazeContext context,
+      String binaryPath,
       WorkspaceRoot workspaceRoot,
       List<String> blazeFlags,
       String key) {
     return BlazeExecutor.getInstance()
         .submit(
             () ->
-                runBlazeInfo(buildSystem, workspaceRoot, key, blazeFlags, context)
+                runBlazeInfo(binaryPath, workspaceRoot, key, blazeFlags, context)
                     .toString()
                     .trim());
   }
 
   @Override
   public ListenableFuture<byte[]> runBlazeInfoGetBytes(
-      @Nullable BlazeContext context,
-      BuildSystem buildSystem,
+      BlazeContext context,
+      String binaryPath,
       WorkspaceRoot workspaceRoot,
       List<String> blazeFlags,
       String key) {
     return BlazeExecutor.getInstance()
         .submit(
-            () -> runBlazeInfo(buildSystem, workspaceRoot, key, blazeFlags, context).toByteArray());
+            () -> runBlazeInfo(binaryPath, workspaceRoot, key, blazeFlags, context).toByteArray());
   }
 
   @Override
   public ListenableFuture<ImmutableMap<String, String>> runBlazeInfo(
-      @Nullable BlazeContext context,
-      BuildSystem buildSystem,
+      BlazeContext context,
+      String binaryPath,
       WorkspaceRoot workspaceRoot,
       List<String> blazeFlags) {
     return BlazeExecutor.getInstance()
         .submit(
             () -> {
               String blazeInfoString =
-                  runBlazeInfo(buildSystem, workspaceRoot, null /* key */, blazeFlags, context)
+                  runBlazeInfo(binaryPath, workspaceRoot, /* key */ null, blazeFlags, context)
                       .toString()
                       .trim();
               return parseBlazeInfoResult(blazeInfoString);
@@ -77,29 +78,28 @@
   }
 
   private static ByteArrayOutputStream runBlazeInfo(
-      BuildSystem buildSystem,
+      String binaryPath,
       WorkspaceRoot workspaceRoot,
       @Nullable String key,
       List<String> blazeFlags,
-      @Nullable BlazeContext context)
+      BlazeContext context)
       throws BlazeInfoException {
-    BlazeCommand.Builder builder = BlazeCommand.builder(buildSystem, BlazeCommandName.INFO);
+    BlazeCommand.Builder builder = BlazeCommand.builder(binaryPath, BlazeCommandName.INFO);
     if (key != null) {
       builder.addBlazeFlags(key);
     }
     BlazeCommand command = builder.addBlazeFlags(blazeFlags).build();
     ByteArrayOutputStream stdout = new ByteArrayOutputStream();
-    ByteArrayOutputStream stderr = new ByteArrayOutputStream();
     int exitCode =
         ExternalTask.builder(workspaceRoot)
             .addBlazeCommand(command)
             .context(context)
             .stdout(stdout)
-            .stderr(stderr)
+            .stderr(LineProcessingOutputStream.of(new PrintOutputLineProcessor(context)))
             .build()
             .run();
     if (exitCode != 0) {
-      throw new BlazeInfoException(exitCode, stdout.toString(), stderr.toString());
+      throw new BlazeInfoException(exitCode, stdout.toString());
     }
     return stdout;
   }
diff --git a/base/src/com/google/idea/blaze/base/console/BlazeConsoleService.java b/base/src/com/google/idea/blaze/base/console/BlazeConsoleService.java
index 2bd7d57..2e40a53 100644
--- a/base/src/com/google/idea/blaze/base/console/BlazeConsoleService.java
+++ b/base/src/com/google/idea/blaze/base/console/BlazeConsoleService.java
@@ -18,8 +18,8 @@
 import com.intellij.execution.ui.ConsoleViewContentType;
 import com.intellij.openapi.components.ServiceManager;
 import com.intellij.openapi.project.Project;
+import javax.annotation.Nullable;
 import org.jetbrains.annotations.NotNull;
-import org.jetbrains.annotations.Nullable;
 
 /** Prints text to the blaze console. */
 public interface BlazeConsoleService {
diff --git a/base/src/com/google/idea/blaze/base/console/BlazeConsoleServiceImpl.java b/base/src/com/google/idea/blaze/base/console/BlazeConsoleServiceImpl.java
index ba5e3a6..5b4792b 100644
--- a/base/src/com/google/idea/blaze/base/console/BlazeConsoleServiceImpl.java
+++ b/base/src/com/google/idea/blaze/base/console/BlazeConsoleServiceImpl.java
@@ -19,8 +19,8 @@
 import com.intellij.openapi.project.Project;
 import com.intellij.openapi.wm.ToolWindow;
 import com.intellij.openapi.wm.ToolWindowManager;
+import javax.annotation.Nullable;
 import org.jetbrains.annotations.NotNull;
-import org.jetbrains.annotations.Nullable;
 
 /** Implementation for BlazeConsoleService */
 public class BlazeConsoleServiceImpl implements BlazeConsoleService {
diff --git a/base/src/com/google/idea/blaze/base/console/BlazeConsoleView.java b/base/src/com/google/idea/blaze/base/console/BlazeConsoleView.java
index d5c02c3..58162c5 100644
--- a/base/src/com/google/idea/blaze/base/console/BlazeConsoleView.java
+++ b/base/src/com/google/idea/blaze/base/console/BlazeConsoleView.java
@@ -38,10 +38,10 @@
 import com.intellij.ui.content.Content;
 import com.intellij.ui.content.ContentFactory;
 import java.awt.BorderLayout;
+import javax.annotation.Nullable;
 import javax.swing.JComponent;
 import javax.swing.JPanel;
 import org.jetbrains.annotations.NotNull;
-import org.jetbrains.annotations.Nullable;
 
 class BlazeConsoleView implements Disposable {
 
diff --git a/base/src/com/google/idea/blaze/base/filecache/FileDiffer.java b/base/src/com/google/idea/blaze/base/filecache/FileDiffer.java
index 0142f06..052bb6e 100644
--- a/base/src/com/google/idea/blaze/base/filecache/FileDiffer.java
+++ b/base/src/com/google/idea/blaze/base/filecache/FileDiffer.java
@@ -23,7 +23,7 @@
 import java.util.List;
 import java.util.Map;
 import java.util.Set;
-import org.jetbrains.annotations.Nullable;
+import javax.annotation.Nullable;
 
 /** Provides a diffing service for a collection of files. */
 public final class FileDiffer {
diff --git a/base/src/com/google/idea/blaze/base/ide/NewBlazePackageDialog.java b/base/src/com/google/idea/blaze/base/ide/NewBlazePackageDialog.java
index 1f5d7c5..356b284 100644
--- a/base/src/com/google/idea/blaze/base/ide/NewBlazePackageDialog.java
+++ b/base/src/com/google/idea/blaze/base/ide/NewBlazePackageDialog.java
@@ -38,11 +38,11 @@
 import java.awt.GridBagLayout;
 import java.io.File;
 import java.util.List;
+import javax.annotation.Nullable;
 import javax.swing.Icon;
 import javax.swing.JComponent;
 import javax.swing.JPanel;
 import org.jetbrains.annotations.NotNull;
-import org.jetbrains.annotations.Nullable;
 
 class NewBlazePackageDialog extends DialogWrapper {
   private static final Logger logger = Logger.getInstance(NewBlazePackageDialog.class);
diff --git a/base/src/com/google/idea/blaze/base/ide/NewRuleUI.java b/base/src/com/google/idea/blaze/base/ide/NewRuleUI.java
index 8f859fd..bf0c26a 100644
--- a/base/src/com/google/idea/blaze/base/ide/NewRuleUI.java
+++ b/base/src/com/google/idea/blaze/base/ide/NewRuleUI.java
@@ -27,9 +27,9 @@
 import com.intellij.ui.components.JBTextField;
 import java.util.Collection;
 import java.util.List;
+import javax.annotation.Nullable;
 import javax.swing.JPanel;
 import org.jetbrains.annotations.NotNull;
-import org.jetbrains.annotations.Nullable;
 
 final class NewRuleUI {
 
diff --git a/base/src/com/google/idea/blaze/base/ideinfo/AndroidIdeInfo.java b/base/src/com/google/idea/blaze/base/ideinfo/AndroidIdeInfo.java
index 760fac7..68a7565 100644
--- a/base/src/com/google/idea/blaze/base/ideinfo/AndroidIdeInfo.java
+++ b/base/src/com/google/idea/blaze/base/ideinfo/AndroidIdeInfo.java
@@ -19,7 +19,7 @@
 import com.google.idea.blaze.base.model.primitives.Label;
 import java.io.Serializable;
 import java.util.Collection;
-import org.jetbrains.annotations.Nullable;
+import javax.annotation.Nullable;
 
 /** Ide info specific to android rules. */
 public final class AndroidIdeInfo implements Serializable {
diff --git a/base/src/com/google/idea/blaze/base/ideinfo/JavaIdeInfo.java b/base/src/com/google/idea/blaze/base/ideinfo/JavaIdeInfo.java
index 174db51..271e390 100644
--- a/base/src/com/google/idea/blaze/base/ideinfo/JavaIdeInfo.java
+++ b/base/src/com/google/idea/blaze/base/ideinfo/JavaIdeInfo.java
@@ -18,7 +18,7 @@
 import com.google.common.collect.ImmutableList;
 import java.io.Serializable;
 import java.util.Collection;
-import org.jetbrains.annotations.Nullable;
+import javax.annotation.Nullable;
 
 /** Ide info specific to java rules. */
 public final class JavaIdeInfo implements Serializable {
diff --git a/base/src/com/google/idea/blaze/base/ideinfo/LibraryArtifact.java b/base/src/com/google/idea/blaze/base/ideinfo/LibraryArtifact.java
index f661e5a..6eaab56 100644
--- a/base/src/com/google/idea/blaze/base/ideinfo/LibraryArtifact.java
+++ b/base/src/com/google/idea/blaze/base/ideinfo/LibraryArtifact.java
@@ -17,7 +17,7 @@
 
 import com.google.common.base.Objects;
 import java.io.Serializable;
-import org.jetbrains.annotations.Nullable;
+import javax.annotation.Nullable;
 
 /** Represents a jar artifact. */
 public class LibraryArtifact implements Serializable {
diff --git a/base/src/com/google/idea/blaze/base/ideinfo/TargetKey.java b/base/src/com/google/idea/blaze/base/ideinfo/TargetKey.java
index d1e99a1..9803427 100644
--- a/base/src/com/google/idea/blaze/base/ideinfo/TargetKey.java
+++ b/base/src/com/google/idea/blaze/base/ideinfo/TargetKey.java
@@ -24,7 +24,7 @@
 import java.io.Serializable;
 import java.util.List;
 
-/** A key that uniquely idenfifies a target in the target map */
+/** A key that uniquely identifies a target in the target map */
 public class TargetKey implements Serializable, Comparable<TargetKey> {
   private static final long serialVersionUID = 3L;
 
diff --git a/base/src/com/google/idea/blaze/base/lang/buildfile/completion/CompletionResultsProcessor.java b/base/src/com/google/idea/blaze/base/lang/buildfile/completion/CompletionResultsProcessor.java
index c5a32e5..b9e6172 100644
--- a/base/src/com/google/idea/blaze/base/lang/buildfile/completion/CompletionResultsProcessor.java
+++ b/base/src/com/google/idea/blaze/base/lang/buildfile/completion/CompletionResultsProcessor.java
@@ -32,10 +32,13 @@
   private final Map<String, LookupElement> results = Maps.newHashMap();
   private final PsiElement originalElement;
   private final QuoteType quoteType;
+  private final boolean allowPrivateSymbols;
 
-  public CompletionResultsProcessor(PsiElement originalElement, QuoteType quoteType) {
+  public CompletionResultsProcessor(
+      PsiElement originalElement, QuoteType quoteType, boolean allowPrivateSymbols) {
     this.originalElement = originalElement;
     this.quoteType = quoteType;
+    this.allowPrivateSymbols = allowPrivateSymbols;
   }
 
   @Override
@@ -49,9 +52,11 @@
       results.put(string, new LoadedSymbolReferenceLookupElement(loadedSymbol, string, quoteType));
     } else if (buildElement instanceof PsiNamedElement) {
       PsiNamedElement namedElement = (PsiNamedElement) buildElement;
-      results.put(
-          namedElement.getName(),
-          new NamedBuildLookupElement((PsiNamedElement) buildElement, quoteType));
+      String name = namedElement.getName();
+      if (!allowPrivateSymbols && name != null && name.startsWith("_")) {
+        return true;
+      }
+      results.put(name, new NamedBuildLookupElement((PsiNamedElement) buildElement, quoteType));
     }
     return true;
   }
diff --git a/base/src/com/google/idea/blaze/base/lang/buildfile/completion/FilterPatterns.java b/base/src/com/google/idea/blaze/base/lang/buildfile/completion/FilterPatterns.java
deleted file mode 100644
index d888efb..0000000
--- a/base/src/com/google/idea/blaze/base/lang/buildfile/completion/FilterPatterns.java
+++ /dev/null
@@ -1,46 +0,0 @@
-/*
- * Copyright 2016 The Bazel Authors. All rights reserved.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *    http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.google.idea.blaze.base.lang.buildfile.completion;
-
-import static com.intellij.patterns.PlatformPatterns.psiElement;
-
-import com.intellij.patterns.ElementPattern;
-import com.intellij.patterns.PatternCondition;
-import com.intellij.psi.PsiElement;
-import com.intellij.util.ProcessingContext;
-import org.jetbrains.annotations.NotNull;
-
-/** Filter patterns used by completion contributors. */
-public class FilterPatterns {
-
-  public static final ElementPattern<PsiElement> indexInParentsChildren(final int childIndex) {
-    return psiElement()
-        .with(
-            new PatternCondition<PsiElement>("isIndexInParentsChildren") {
-              @Override
-              public boolean accepts(@NotNull PsiElement psiElement, ProcessingContext context) {
-                final PsiElement parent = psiElement.getParent();
-                if (parent != null) {
-                  final PsiElement[] children = parent.getChildren();
-                  if (childIndex < children.length && psiElement.equals(children[childIndex])) {
-                    return true;
-                  }
-                }
-                return false;
-              }
-            });
-  }
-}
diff --git a/base/src/com/google/idea/blaze/base/lang/buildfile/findusages/BuildTargetElementEvaluator.java b/base/src/com/google/idea/blaze/base/lang/buildfile/findusages/BuildTargetElementEvaluator.java
index 7ef9399..a538b35 100644
--- a/base/src/com/google/idea/blaze/base/lang/buildfile/findusages/BuildTargetElementEvaluator.java
+++ b/base/src/com/google/idea/blaze/base/lang/buildfile/findusages/BuildTargetElementEvaluator.java
@@ -56,35 +56,32 @@
   }
 
   private static final Comparator<PsiReference> COMPARATOR =
-      new Comparator<PsiReference>() {
-        @Override
-        public int compare(final PsiReference ref1, final PsiReference ref2) {
-          boolean resolves1 = ref1.resolve() != null;
-          boolean resolves2 = ref2.resolve() != null;
-          if (resolves1 && !resolves2) {
-            return -1;
-          }
-          if (!resolves1 && resolves2) {
-            return 1;
-          }
+      (ref1, ref2) -> {
+        boolean resolves1 = ref1.resolve() != null;
+        boolean resolves2 = ref2.resolve() != null;
+        if (resolves1 && !resolves2) {
+          return -1;
+        }
+        if (!resolves1 && resolves2) {
+          return 1;
+        }
 
-          final TextRange range1 = ref1.getRangeInElement();
-          final TextRange range2 = ref2.getRangeInElement();
+        final TextRange range1 = ref1.getRangeInElement();
+        final TextRange range2 = ref2.getRangeInElement();
 
-          if (TextRange.areSegmentsEqual(range1, range2)) {
-            return 0;
-          }
-          if (range1.getStartOffset() >= range2.getStartOffset()
-              && range1.getEndOffset() <= range2.getEndOffset()) {
-            return 1;
-          }
-          if (range2.getStartOffset() >= range1.getStartOffset()
-              && range2.getEndOffset() <= range1.getEndOffset()) {
-            return -1;
-          }
-
+        if (TextRange.areSegmentsEqual(range1, range2)) {
           return 0;
         }
+        if (range1.getStartOffset() >= range2.getStartOffset()
+            && range1.getEndOffset() <= range2.getEndOffset()) {
+          return 1;
+        }
+        if (range2.getStartOffset() >= range1.getStartOffset()
+            && range2.getEndOffset() <= range1.getEndOffset()) {
+          return -1;
+        }
+
+        return 0;
       };
 
   /** Redirect 'name' funcall argument values to the funcall expression (b/29088829). */
diff --git a/base/src/com/google/idea/blaze/base/lang/buildfile/formatting/BuildCommenter.java b/base/src/com/google/idea/blaze/base/lang/buildfile/formatting/BuildCommenter.java
index 34af8b9..994107e 100644
--- a/base/src/com/google/idea/blaze/base/lang/buildfile/formatting/BuildCommenter.java
+++ b/base/src/com/google/idea/blaze/base/lang/buildfile/formatting/BuildCommenter.java
@@ -20,7 +20,7 @@
 import com.intellij.lang.CodeDocumentationAwareCommenter;
 import com.intellij.psi.PsiComment;
 import com.intellij.psi.tree.IElementType;
-import org.jetbrains.annotations.Nullable;
+import javax.annotation.Nullable;
 
 /** Supports (un)commenting lines via IntelliJ */
 public class BuildCommenter implements CodeDocumentationAwareCommenter {
diff --git a/base/src/com/google/idea/blaze/base/lang/buildfile/formatting/BuildLanguageCodeStyleSettingsProvider.java b/base/src/com/google/idea/blaze/base/lang/buildfile/formatting/BuildLanguageCodeStyleSettingsProvider.java
index b2295af..2562a55 100644
--- a/base/src/com/google/idea/blaze/base/lang/buildfile/formatting/BuildLanguageCodeStyleSettingsProvider.java
+++ b/base/src/com/google/idea/blaze/base/lang/buildfile/formatting/BuildLanguageCodeStyleSettingsProvider.java
@@ -21,8 +21,8 @@
 import com.intellij.lang.Language;
 import com.intellij.psi.codeStyle.CommonCodeStyleSettings;
 import com.intellij.psi.codeStyle.LanguageCodeStyleSettingsProvider;
+import javax.annotation.Nullable;
 import org.jetbrains.annotations.NotNull;
-import org.jetbrains.annotations.Nullable;
 
 /** Allows BUILD language-specific code style settings */
 public class BuildLanguageCodeStyleSettingsProvider extends LanguageCodeStyleSettingsProvider {
diff --git a/base/src/com/google/idea/blaze/base/lang/buildfile/language/BuildFileTypeFactory.java b/base/src/com/google/idea/blaze/base/lang/buildfile/language/BuildFileTypeFactory.java
index 4c4a21c..e975e61 100644
--- a/base/src/com/google/idea/blaze/base/lang/buildfile/language/BuildFileTypeFactory.java
+++ b/base/src/com/google/idea/blaze/base/lang/buildfile/language/BuildFileTypeFactory.java
@@ -15,8 +15,10 @@
  */
 package com.google.idea.blaze.base.lang.buildfile.language;
 
+import com.google.common.collect.ImmutableList;
 import com.google.idea.blaze.base.bazel.BuildSystemProvider;
 import com.intellij.openapi.fileTypes.ExtensionFileNameMatcher;
+import com.intellij.openapi.fileTypes.FileNameMatcher;
 import com.intellij.openapi.fileTypes.FileTypeConsumer;
 import com.intellij.openapi.fileTypes.FileTypeFactory;
 import org.jetbrains.annotations.NotNull;
@@ -26,9 +28,11 @@
 
   @Override
   public void createFileTypes(@NotNull final FileTypeConsumer consumer) {
-    consumer.consume(
-        BuildFileType.INSTANCE,
-        BuildSystemProvider.defaultBuildSystem().buildFileMatcher(),
-        new ExtensionFileNameMatcher("bzl"));
+    ImmutableList<FileNameMatcher> fileNameMatchers =
+        ImmutableList.<FileNameMatcher>builder()
+            .addAll(BuildSystemProvider.defaultBuildSystem().buildFileMatchers())
+            .add(new ExtensionFileNameMatcher("bzl"))
+            .build();
+    consumer.consume(BuildFileType.INSTANCE, fileNameMatchers.toArray(new FileNameMatcher[0]));
   }
 }
diff --git a/base/src/com/google/idea/blaze/base/lang/buildfile/psi/AssignmentStatement.java b/base/src/com/google/idea/blaze/base/lang/buildfile/psi/AssignmentStatement.java
index fc0d2e1..b1e34eb 100644
--- a/base/src/com/google/idea/blaze/base/lang/buildfile/psi/AssignmentStatement.java
+++ b/base/src/com/google/idea/blaze/base/lang/buildfile/psi/AssignmentStatement.java
@@ -16,6 +16,7 @@
 package com.google.idea.blaze.base.lang.buildfile.psi;
 
 import com.intellij.lang.ASTNode;
+import com.intellij.psi.PsiElement;
 import com.intellij.util.PlatformIcons;
 import javax.annotation.Nullable;
 import javax.swing.Icon;
@@ -36,7 +37,8 @@
   /** Returns the RHS of the assignment */
   @Nullable
   public Expression getAssignedValue() {
-    return childToPsi(BuildElementTypes.EXPRESSIONS, 1);
+    PsiElement psi = childToPsi(BuildElementTypes.EXPRESSIONS, 1);
+    return psi instanceof Expression ? (Expression) psi : null;
   }
 
   @Override
diff --git a/base/src/com/google/idea/blaze/base/lang/buildfile/psi/AugmentedAssignmentStatement.java b/base/src/com/google/idea/blaze/base/lang/buildfile/psi/AugmentedAssignmentStatement.java
index 5d2384d..c3a3eb0 100644
--- a/base/src/com/google/idea/blaze/base/lang/buildfile/psi/AugmentedAssignmentStatement.java
+++ b/base/src/com/google/idea/blaze/base/lang/buildfile/psi/AugmentedAssignmentStatement.java
@@ -16,6 +16,7 @@
 package com.google.idea.blaze.base.lang.buildfile.psi;
 
 import com.intellij.lang.ASTNode;
+import com.intellij.psi.PsiElement;
 import javax.annotation.Nullable;
 
 /** PSI element for an augmented assignment statement [expr += expr] */
@@ -28,13 +29,15 @@
   /** Returns the LHS of the assignment */
   @Nullable
   public TargetExpression getLeftHandSideExpression() {
-    return childToPsi(BuildElementTypes.EXPRESSIONS, 0);
+    PsiElement psi = childToPsi(BuildElementTypes.EXPRESSIONS, 0);
+    return psi instanceof TargetExpression ? (TargetExpression) psi : null;
   }
 
   /** Returns the RHS of the assignment */
   @Nullable
   public Expression getAssignedValue() {
-    return childToPsi(BuildElementTypes.EXPRESSIONS, 1);
+    PsiElement psi = childToPsi(BuildElementTypes.EXPRESSIONS, 1);
+    return psi instanceof Expression ? (Expression) psi : null;
   }
 
   @Override
diff --git a/base/src/com/google/idea/blaze/base/lang/buildfile/psi/BinaryOpExpression.java b/base/src/com/google/idea/blaze/base/lang/buildfile/psi/BinaryOpExpression.java
index 76a4387..3dab6d8 100644
--- a/base/src/com/google/idea/blaze/base/lang/buildfile/psi/BinaryOpExpression.java
+++ b/base/src/com/google/idea/blaze/base/lang/buildfile/psi/BinaryOpExpression.java
@@ -16,6 +16,7 @@
 package com.google.idea.blaze.base.lang.buildfile.psi;
 
 import com.intellij.lang.ASTNode;
+import com.intellij.psi.PsiElement;
 import javax.annotation.Nullable;
 
 /** PSI element for an binary operation expression [expr BIN_OP expr] */
@@ -33,12 +34,14 @@
   /** Returns the LHS of the expression */
   @Nullable
   public Expression getLhs() {
-    return childToPsi(BuildElementTypes.EXPRESSIONS, 0);
+    PsiElement psi = childToPsi(BuildElementTypes.EXPRESSIONS, 0);
+    return psi instanceof Expression ? (Expression) psi : null;
   }
 
   /** Returns the RHS of the expression */
   @Nullable
   public Expression getRhs() {
-    return childToPsi(BuildElementTypes.EXPRESSIONS, 1);
+    PsiElement psi = childToPsi(BuildElementTypes.EXPRESSIONS, 1);
+    return psi instanceof Expression ? (Expression) psi : null;
   }
 }
diff --git a/base/src/com/google/idea/blaze/base/lang/buildfile/psi/BuildElementImpl.java b/base/src/com/google/idea/blaze/base/lang/buildfile/psi/BuildElementImpl.java
index 55f472d..78990fb 100644
--- a/base/src/com/google/idea/blaze/base/lang/buildfile/psi/BuildElementImpl.java
+++ b/base/src/com/google/idea/blaze/base/lang/buildfile/psi/BuildElementImpl.java
@@ -82,13 +82,14 @@
     return psiElements;
   }
 
+  /** Finds the n'th child of the specified type */
   @Nullable
-  protected <T extends BuildElement> T childToPsi(TokenSet filterSet, int index) {
+  protected PsiElement childToPsi(TokenSet filterSet, int index) {
     final ASTNode[] nodes = getNode().getChildren(filterSet);
     if (nodes.length <= index) {
       return null;
     }
-    return (T) nodes[index].getPsi();
+    return nodes[index].getPsi();
   }
 
   @Nullable
@@ -160,8 +161,8 @@
   @Nullable
   @Override
   public WorkspacePath getWorkspacePath() {
-    BuildFile file = (BuildFile) getContainingFile();
-    return file.getWorkspacePath();
+    BuildFile file = getContainingFile();
+    return file != null ? file.getWorkspacePath() : null;
   }
 
   @Nullable
diff --git a/base/src/com/google/idea/blaze/base/lang/buildfile/psi/FuncallExpression.java b/base/src/com/google/idea/blaze/base/lang/buildfile/psi/FuncallExpression.java
index e59d9a7..713807a 100644
--- a/base/src/com/google/idea/blaze/base/lang/buildfile/psi/FuncallExpression.java
+++ b/base/src/com/google/idea/blaze/base/lang/buildfile/psi/FuncallExpression.java
@@ -15,8 +15,6 @@
  */
 package com.google.idea.blaze.base.lang.buildfile.psi;
 
-import com.google.idea.blaze.base.lang.buildfile.language.semantics.BuildLanguageSpec;
-import com.google.idea.blaze.base.lang.buildfile.language.semantics.BuildLanguageSpecProvider;
 import com.google.idea.blaze.base.lang.buildfile.psi.util.PsiUtils;
 import com.google.idea.blaze.base.lang.buildfile.references.FuncallReference;
 import com.google.idea.blaze.base.lang.buildfile.references.LabelUtils;
@@ -175,11 +173,6 @@
     if (nameNode == null) {
       return null;
     }
-    BuildLanguageSpec spec = BuildLanguageSpecProvider.getInstance().getLanguageSpec(getProject());
-    if (spec != null && spec.hasRule(nameNode.getText())) {
-      // don't try to follow references to built-in rules
-      return null;
-    }
     TextRange range = PsiUtils.childRangeInParent(getTextRange(), nameNode.getTextRange());
     return new FuncallReference(this, range);
   }
diff --git a/base/src/com/google/idea/blaze/base/lang/buildfile/psi/FunctionStatement.java b/base/src/com/google/idea/blaze/base/lang/buildfile/psi/FunctionStatement.java
index 0dce2fe..3865d64 100644
--- a/base/src/com/google/idea/blaze/base/lang/buildfile/psi/FunctionStatement.java
+++ b/base/src/com/google/idea/blaze/base/lang/buildfile/psi/FunctionStatement.java
@@ -19,8 +19,8 @@
 import com.intellij.lang.ASTNode;
 import com.intellij.psi.PsiElement;
 import com.intellij.util.PlatformIcons;
+import javax.annotation.Nullable;
 import javax.swing.Icon;
-import org.jetbrains.annotations.Nullable;
 
 /** PSI element for a function definition statement. */
 public class FunctionStatement extends NamedBuildElement
diff --git a/base/src/com/google/idea/blaze/base/lang/buildfile/psi/Parameter.java b/base/src/com/google/idea/blaze/base/lang/buildfile/psi/Parameter.java
index c873bb6..37298d2 100644
--- a/base/src/com/google/idea/blaze/base/lang/buildfile/psi/Parameter.java
+++ b/base/src/com/google/idea/blaze/base/lang/buildfile/psi/Parameter.java
@@ -19,8 +19,8 @@
 import com.google.idea.blaze.base.lang.buildfile.lexer.TokenKind;
 import com.intellij.icons.AllIcons;
 import com.intellij.lang.ASTNode;
+import javax.annotation.Nullable;
 import javax.swing.Icon;
-import org.jetbrains.annotations.Nullable;
 
 /** PSI nodes for parameters in a function declaration */
 public abstract class Parameter extends NamedBuildElement {
diff --git a/base/src/com/google/idea/blaze/base/lang/buildfile/psi/ReferenceExpression.java b/base/src/com/google/idea/blaze/base/lang/buildfile/psi/ReferenceExpression.java
index 287402c..755231e 100644
--- a/base/src/com/google/idea/blaze/base/lang/buildfile/psi/ReferenceExpression.java
+++ b/base/src/com/google/idea/blaze/base/lang/buildfile/psi/ReferenceExpression.java
@@ -20,7 +20,7 @@
 import com.intellij.lang.ASTNode;
 import com.intellij.psi.PsiReference;
 import com.intellij.psi.tree.IElementType;
-import org.jetbrains.annotations.Nullable;
+import javax.annotation.Nullable;
 
 /** References a PsiNamedElement */
 public class ReferenceExpression extends BuildElementImpl implements Expression {
diff --git a/base/src/com/google/idea/blaze/base/lang/buildfile/psi/ReturnStatement.java b/base/src/com/google/idea/blaze/base/lang/buildfile/psi/ReturnStatement.java
index a334c00..63926ee 100644
--- a/base/src/com/google/idea/blaze/base/lang/buildfile/psi/ReturnStatement.java
+++ b/base/src/com/google/idea/blaze/base/lang/buildfile/psi/ReturnStatement.java
@@ -16,6 +16,7 @@
 package com.google.idea.blaze.base.lang.buildfile.psi;
 
 import com.intellij.lang.ASTNode;
+import com.intellij.psi.PsiElement;
 import javax.annotation.Nullable;
 
 /** A wrapper Statement class for return expressions. */
@@ -27,7 +28,8 @@
 
   @Nullable
   public Expression getReturnExpression() {
-    return childToPsi(BuildElementTypes.EXPRESSIONS, 0);
+    PsiElement psi = childToPsi(BuildElementTypes.EXPRESSIONS, 0);
+    return psi instanceof Expression ? (Expression) psi : null;
   }
 
   @Override
diff --git a/base/src/com/google/idea/blaze/base/lang/buildfile/psi/TargetExpression.java b/base/src/com/google/idea/blaze/base/lang/buildfile/psi/TargetExpression.java
index 405d0de..96a87ec 100644
--- a/base/src/com/google/idea/blaze/base/lang/buildfile/psi/TargetExpression.java
+++ b/base/src/com/google/idea/blaze/base/lang/buildfile/psi/TargetExpression.java
@@ -19,8 +19,8 @@
 import com.intellij.lang.ASTNode;
 import com.intellij.psi.PsiReference;
 import com.intellij.util.PlatformIcons;
+import javax.annotation.Nullable;
 import javax.swing.Icon;
-import org.jetbrains.annotations.Nullable;
 
 /** References a PsiNamedElement */
 public class TargetExpression extends NamedBuildElement implements Expression {
diff --git a/base/src/com/google/idea/blaze/base/lang/buildfile/quickfix/DeprecatedLoadQuickFix.java b/base/src/com/google/idea/blaze/base/lang/buildfile/quickfix/DeprecatedLoadQuickFix.java
new file mode 100644
index 0000000..c58a039
--- /dev/null
+++ b/base/src/com/google/idea/blaze/base/lang/buildfile/quickfix/DeprecatedLoadQuickFix.java
@@ -0,0 +1,104 @@
+/*
+ * Copyright 2017 The Bazel Authors. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.idea.blaze.base.lang.buildfile.quickfix;
+
+import com.google.idea.blaze.base.bazel.BuildSystemProvider;
+import com.google.idea.blaze.base.lang.buildfile.psi.StringLiteral;
+import com.google.idea.blaze.base.lang.buildfile.psi.util.PsiUtils;
+import com.google.idea.blaze.base.model.primitives.WorkspacePath;
+import com.google.idea.blaze.base.model.primitives.WorkspaceRoot;
+import com.google.idea.blaze.base.settings.Blaze;
+import com.intellij.codeInsight.intention.HighPriorityAction;
+import com.intellij.codeInspection.LocalQuickFix;
+import com.intellij.codeInspection.ProblemDescriptor;
+import com.intellij.history.core.Paths;
+import com.intellij.lang.ASTNode;
+import com.intellij.openapi.project.Project;
+import com.intellij.psi.PsiElement;
+import java.io.File;
+import javax.annotation.Nullable;
+
+/** Offer to convert deprecated statements to label format. */
+public class DeprecatedLoadQuickFix implements LocalQuickFix, HighPriorityAction {
+
+  public static final DeprecatedLoadQuickFix INSTANCE = new DeprecatedLoadQuickFix();
+
+  @Override
+  public String getFamilyName() {
+    return "Fix load statement format";
+  }
+
+  @Override
+  public String getName() {
+    return getFamilyName();
+  }
+
+  @Override
+  public void applyFix(Project project, ProblemDescriptor descriptor) {
+    PsiElement element = descriptor.getPsiElement();
+    if (element instanceof StringLiteral) {
+      fixLoadString(project, (StringLiteral) element);
+    }
+  }
+
+  private static void fixLoadString(Project project, StringLiteral importString) {
+    String contents = importString.getStringContents();
+    if (!contents.startsWith("/") || contents.startsWith("//")) {
+      return;
+    }
+    WorkspaceRoot root = WorkspaceRoot.fromProjectSafe(project);
+    if (root == null) {
+      return;
+    }
+    WorkspacePath workspacePath = WorkspacePath.createIfValid(contents.substring(1));
+    if (workspacePath == null) {
+      return;
+    }
+    File file = root.fileForPath(workspacePath);
+    File parentPackageFile = findContainingPackage(project, file);
+    if (parentPackageFile == null) {
+      return;
+    }
+    WorkspacePath packagePath = root.workspacePathForSafe(parentPackageFile);
+    if (packagePath == null) {
+      return;
+    }
+    String relativePath =
+        Paths.relativeIfUnder(workspacePath.relativePath(), packagePath.relativePath());
+    if (relativePath == null) {
+      return;
+    }
+    String newString = "//" + packagePath + ":" + relativePath;
+
+    ASTNode node = importString.getNode();
+    node.replaceChild(
+        node.getFirstChildNode(), PsiUtils.createNewLabel(importString.getProject(), newString));
+  }
+
+  @Nullable
+  private static File findContainingPackage(Project project, File file) {
+    BuildSystemProvider provider = Blaze.getBuildSystemProvider(project);
+    file = file.getParentFile();
+    while (file != null) {
+      File buildFile = provider.findBuildFileInDirectory(file);
+      if (buildFile != null) {
+        return file;
+      }
+      file = file.getParentFile();
+    }
+    return null;
+  }
+}
diff --git a/base/src/com/google/idea/blaze/base/lang/buildfile/references/FileLookupData.java b/base/src/com/google/idea/blaze/base/lang/buildfile/references/FileLookupData.java
index eabe428..8a21adb 100644
--- a/base/src/com/google/idea/blaze/base/lang/buildfile/references/FileLookupData.java
+++ b/base/src/com/google/idea/blaze/base/lang/buildfile/references/FileLookupData.java
@@ -32,8 +32,8 @@
 import com.intellij.util.PathUtil;
 import com.intellij.util.PlatformIcons;
 import icons.BlazeIcons;
+import javax.annotation.Nullable;
 import javax.swing.Icon;
-import org.jetbrains.annotations.Nullable;
 
 /** The data relevant to finding file lookups. */
 public class FileLookupData {
diff --git a/base/src/com/google/idea/blaze/base/lang/buildfile/references/FuncallReference.java b/base/src/com/google/idea/blaze/base/lang/buildfile/references/FuncallReference.java
index 526e2ec..b3d69dd 100644
--- a/base/src/com/google/idea/blaze/base/lang/buildfile/references/FuncallReference.java
+++ b/base/src/com/google/idea/blaze/base/lang/buildfile/references/FuncallReference.java
@@ -31,7 +31,7 @@
 public class FuncallReference extends PsiReferenceBase<FuncallExpression> {
 
   public FuncallReference(FuncallExpression element, TextRange rangeInElement) {
-    super(element, rangeInElement, /*soft*/ true);
+    super(element, rangeInElement, /* soft */ true);
   }
 
   @Nullable
diff --git a/base/src/com/google/idea/blaze/base/lang/buildfile/references/LabelReference.java b/base/src/com/google/idea/blaze/base/lang/buildfile/references/LabelReference.java
index f1e6542..9bbf574 100644
--- a/base/src/com/google/idea/blaze/base/lang/buildfile/references/LabelReference.java
+++ b/base/src/com/google/idea/blaze/base/lang/buildfile/references/LabelReference.java
@@ -35,8 +35,8 @@
 import com.intellij.psi.PsiReferenceBase;
 import com.intellij.util.ArrayUtil;
 import com.intellij.util.IncorrectOperationException;
+import javax.annotation.Nullable;
 import org.jetbrains.annotations.NotNull;
-import org.jetbrains.annotations.Nullable;
 
 /** Converts a blaze label into an absolute path, then resolves that path to a PsiElements */
 public class LabelReference extends PsiReferenceBase<StringLiteral> {
diff --git a/base/src/com/google/idea/blaze/base/lang/buildfile/references/LoadedSymbolReference.java b/base/src/com/google/idea/blaze/base/lang/buildfile/references/LoadedSymbolReference.java
index 219d843..44fa77f 100644
--- a/base/src/com/google/idea/blaze/base/lang/buildfile/references/LoadedSymbolReference.java
+++ b/base/src/com/google/idea/blaze/base/lang/buildfile/references/LoadedSymbolReference.java
@@ -33,7 +33,7 @@
   private final LabelReference bzlFileReference;
 
   public LoadedSymbolReference(StringLiteral element, LabelReference bzlFileReference) {
-    super(element, new TextRange(0, element.getTextLength()), /*soft*/ false);
+    super(element, new TextRange(0, element.getTextLength()), /* soft */ false);
     this.bzlFileReference = bzlFileReference;
   }
 
@@ -54,7 +54,7 @@
       return EMPTY_ARRAY;
     }
     CompletionResultsProcessor processor =
-        new CompletionResultsProcessor(myElement, myElement.getQuoteType());
+        new CompletionResultsProcessor(myElement, myElement.getQuoteType(), false);
     ((BuildFile) bzlFile).searchSymbolsInScope(processor, null);
     return processor.getResults().toArray();
   }
diff --git a/base/src/com/google/idea/blaze/base/lang/buildfile/references/LocalReference.java b/base/src/com/google/idea/blaze/base/lang/buildfile/references/LocalReference.java
index 85808b1..08b3237 100644
--- a/base/src/com/google/idea/blaze/base/lang/buildfile/references/LocalReference.java
+++ b/base/src/com/google/idea/blaze/base/lang/buildfile/references/LocalReference.java
@@ -33,7 +33,7 @@
 public class LocalReference extends PsiReferenceBase<ReferenceExpression> {
 
   public LocalReference(ReferenceExpression element) {
-    super(element, new TextRange(0, element.getTextLength()), /*soft*/ false);
+    super(element, new TextRange(0, element.getTextLength()), /* soft */ false);
   }
 
   @Nullable
@@ -49,7 +49,7 @@
   @Override
   public Object[] getVariants() {
     CompletionResultsProcessor processor =
-        new CompletionResultsProcessor(myElement, QuoteType.NoQuotes);
+        new CompletionResultsProcessor(myElement, QuoteType.NoQuotes, true);
     ResolveUtil.searchInScope(myElement, processor);
     return processor.getResults().toArray();
   }
diff --git a/base/src/com/google/idea/blaze/base/lang/buildfile/references/TargetReference.java b/base/src/com/google/idea/blaze/base/lang/buildfile/references/TargetReference.java
index 52ee6da..68ba606 100644
--- a/base/src/com/google/idea/blaze/base/lang/buildfile/references/TargetReference.java
+++ b/base/src/com/google/idea/blaze/base/lang/buildfile/references/TargetReference.java
@@ -34,7 +34,7 @@
 public class TargetReference extends PsiReferenceBase<TargetExpression> {
 
   public TargetReference(TargetExpression element) {
-    super(element, new TextRange(0, element.getTextLength()), /*soft*/ true);
+    super(element, new TextRange(0, element.getTextLength()), /* soft */ true);
   }
 
   @Nullable
@@ -51,7 +51,7 @@
   @Override
   public Object[] getVariants() {
     CompletionResultsProcessor processor =
-        new CompletionResultsProcessor(myElement, QuoteType.NoQuotes);
+        new CompletionResultsProcessor(myElement, QuoteType.NoQuotes, true);
     ResolveUtil.searchInScope(myElement, processor);
     return processor.getResults().toArray();
   }
diff --git a/base/src/com/google/idea/blaze/base/lang/buildfile/search/BlazePackage.java b/base/src/com/google/idea/blaze/base/lang/buildfile/search/BlazePackage.java
index 6f1b102..51cf7a7 100644
--- a/base/src/com/google/idea/blaze/base/lang/buildfile/search/BlazePackage.java
+++ b/base/src/com/google/idea/blaze/base/lang/buildfile/search/BlazePackage.java
@@ -15,10 +15,12 @@
  */
 package com.google.idea.blaze.base.lang.buildfile.search;
 
+import com.google.idea.blaze.base.bazel.BuildSystemProvider;
 import com.google.idea.blaze.base.lang.buildfile.psi.BuildFile;
 import com.google.idea.blaze.base.model.primitives.WorkspacePath;
 import com.google.idea.blaze.base.settings.Blaze;
 import com.intellij.history.core.Paths;
+import com.intellij.openapi.roots.ProjectFileIndex;
 import com.intellij.openapi.vfs.VirtualFile;
 import com.intellij.psi.PackagePrefixFileSystemItem;
 import com.intellij.psi.PsiDirectory;
@@ -192,4 +194,27 @@
         "%s package: %s",
         Blaze.buildSystemName(buildFile.getProject()), buildFile.getPackageWorkspacePath());
   }
+
+  public static boolean hasBlazePackageChild(PsiDirectory directory) {
+    ProjectFileIndex index = ProjectFileIndex.SERVICE.getInstance(directory.getProject());
+    BuildSystemProvider buildSystemProvider = Blaze.getBuildSystemProvider(directory.getProject());
+    return hasBlazePackageChild(index, buildSystemProvider, directory);
+  }
+
+  private static boolean hasBlazePackageChild(
+      ProjectFileIndex index, BuildSystemProvider buildSystemProvider, PsiDirectory directory) {
+    if (!index.isInSourceContent(directory.getVirtualFile())) {
+      // only search project files
+      return false;
+    }
+    if (buildSystemProvider.findBuildFileInDirectory(directory.getVirtualFile()) != null) {
+      return true;
+    }
+    for (PsiDirectory child : directory.getSubdirectories()) {
+      if (hasBlazePackageChild(index, buildSystemProvider, child)) {
+        return true;
+      }
+    }
+    return false;
+  }
 }
diff --git a/base/src/com/google/idea/blaze/base/lang/buildfile/search/BuildLabelReferenceSearcher.java b/base/src/com/google/idea/blaze/base/lang/buildfile/search/BuildReferenceSearcher.java
similarity index 93%
rename from base/src/com/google/idea/blaze/base/lang/buildfile/search/BuildLabelReferenceSearcher.java
rename to base/src/com/google/idea/blaze/base/lang/buildfile/search/BuildReferenceSearcher.java
index 688cc95..44af324 100644
--- a/base/src/com/google/idea/blaze/base/lang/buildfile/search/BuildLabelReferenceSearcher.java
+++ b/base/src/com/google/idea/blaze/base/lang/buildfile/search/BuildReferenceSearcher.java
@@ -20,7 +20,7 @@
 import com.google.idea.blaze.base.lang.buildfile.psi.BuildFile;
 import com.google.idea.blaze.base.lang.buildfile.psi.BuildFile.BlazeFileType;
 import com.google.idea.blaze.base.lang.buildfile.psi.FuncallExpression;
-import com.google.idea.blaze.base.lang.buildfile.psi.FunctionStatement;
+import com.google.idea.blaze.base.lang.buildfile.psi.NamedBuildElement;
 import com.google.idea.blaze.base.lang.buildfile.psi.util.PsiUtils;
 import com.google.idea.blaze.base.lang.buildfile.references.LabelUtils;
 import com.google.idea.blaze.base.model.primitives.Label;
@@ -39,23 +39,24 @@
 import java.util.List;
 import javax.annotation.Nullable;
 
-/** String search for label references in BUILD files */
-public class BuildLabelReferenceSearcher extends QueryExecutorBase<PsiReference, SearchParameters> {
+/** String search for references in BUILD files */
+public class BuildReferenceSearcher extends QueryExecutorBase<PsiReference, SearchParameters> {
 
-  public BuildLabelReferenceSearcher() {
+  public BuildReferenceSearcher() {
     super(true);
   }
 
   @Override
   public void processQuery(SearchParameters params, Processor<PsiReference> consumer) {
     PsiElement element = params.getElementToSearch();
-    if (element instanceof FunctionStatement) {
-      String fnName = ((FunctionStatement) element).getName();
+    if (element instanceof NamedBuildElement) {
+      String fnName = ((NamedBuildElement) element).getName();
       if (fnName != null) {
         searchForString(params, element, fnName);
       }
       return;
     }
+
     PsiFile file = ResolveUtil.asFileSearch(element);
     if (file != null) {
       processFileReferences(params, file);
diff --git a/base/src/com/google/idea/blaze/base/lang/buildfile/stubs/BuildFileStubBuilder.java b/base/src/com/google/idea/blaze/base/lang/buildfile/stubs/BuildFileStubBuilder.java
index 73d1e58..4c4a53a 100644
--- a/base/src/com/google/idea/blaze/base/lang/buildfile/stubs/BuildFileStubBuilder.java
+++ b/base/src/com/google/idea/blaze/base/lang/buildfile/stubs/BuildFileStubBuilder.java
@@ -19,7 +19,7 @@
 import com.intellij.psi.stubs.BinaryFileStubBuilder;
 import com.intellij.psi.stubs.Stub;
 import com.intellij.util.indexing.FileContent;
-import org.jetbrains.annotations.Nullable;
+import javax.annotation.Nullable;
 
 /** Empty stub builder to suppress errors when IntelliJ is looking for stubs. */
 public class BuildFileStubBuilder implements BinaryFileStubBuilder {
diff --git a/base/src/com/google/idea/blaze/base/lang/buildfile/sync/BuildLangSyncPlugin.java b/base/src/com/google/idea/blaze/base/lang/buildfile/sync/BuildLangSyncPlugin.java
index 0279a2f..c901eb8 100644
--- a/base/src/com/google/idea/blaze/base/lang/buildfile/sync/BuildLangSyncPlugin.java
+++ b/base/src/com/google/idea/blaze/base/lang/buildfile/sync/BuildLangSyncPlugin.java
@@ -15,8 +15,8 @@
  */
 package com.google.idea.blaze.base.lang.buildfile.sync;
 
-import com.google.common.collect.ImmutableList;
 import com.google.common.util.concurrent.ListenableFuture;
+import com.google.idea.blaze.base.command.BlazeFlags;
 import com.google.idea.blaze.base.command.info.BlazeInfo;
 import com.google.idea.blaze.base.ideinfo.TargetMap;
 import com.google.idea.blaze.base.lang.buildfile.language.semantics.BuildLanguageSpec;
@@ -62,7 +62,7 @@
       @Nullable SyncState previousSyncState) {
 
     LanguageSpecResult spec =
-        getBuildLanguageSpec(project, workspaceRoot, previousSyncState, context);
+        getBuildLanguageSpec(project, workspaceRoot, projectViewSet, previousSyncState, context);
     if (spec != null) {
       syncStateBuilder.put(LanguageSpecResult.class, spec);
     }
@@ -72,6 +72,7 @@
   private static LanguageSpecResult getBuildLanguageSpec(
       Project project,
       WorkspaceRoot workspace,
+      ProjectViewSet projectViewSet,
       @Nullable SyncState previousSyncState,
       BlazeContext parentContext) {
     LanguageSpecResult oldResult =
@@ -84,7 +85,8 @@
             parentContext,
             (context) -> {
               context.push(new TimingScope("BUILD language spec"));
-              BuildLanguageSpec spec = parseLanguageSpec(project, workspace, context);
+              BuildLanguageSpec spec =
+                  parseLanguageSpec(project, workspace, projectViewSet, context);
               if (spec != null) {
                 return new LanguageSpecResult(spec, System.currentTimeMillis());
               }
@@ -95,7 +97,10 @@
 
   @Nullable
   private static BuildLanguageSpec parseLanguageSpec(
-      Project project, WorkspaceRoot workspace, BlazeContext context) {
+      Project project,
+      WorkspaceRoot workspace,
+      ProjectViewSet projectViewSet,
+      BlazeContext context) {
     try {
       // it's wasteful converting to a string and back, but uses existing code,
       // and has a very minor cost (this is only run once per workspace)
@@ -103,9 +108,9 @@
           BlazeInfo.getInstance()
               .runBlazeInfoGetBytes(
                   context,
-                  Blaze.getBuildSystem(project),
+                  Blaze.getBuildSystemProvider(project).getSyncBinaryPath(),
                   workspace,
-                  ImmutableList.of(),
+                  BlazeFlags.buildFlags(project, projectViewSet),
                   BlazeInfo.BUILD_LANGUAGE);
 
       return BuildLanguageSpec.fromProto(Build.BuildLanguage.parseFrom(future.get()));
diff --git a/base/src/com/google/idea/blaze/base/lang/buildfile/validation/BuildAnnotator.java b/base/src/com/google/idea/blaze/base/lang/buildfile/validation/BuildAnnotator.java
index 84028f8..b4b9230 100644
--- a/base/src/com/google/idea/blaze/base/lang/buildfile/validation/BuildAnnotator.java
+++ b/base/src/com/google/idea/blaze/base/lang/buildfile/validation/BuildAnnotator.java
@@ -16,6 +16,7 @@
 package com.google.idea.blaze.base.lang.buildfile.validation;
 
 import com.google.idea.blaze.base.lang.buildfile.psi.BuildElementVisitor;
+import com.intellij.lang.annotation.Annotation;
 import com.intellij.lang.annotation.AnnotationHolder;
 import com.intellij.lang.annotation.Annotator;
 import com.intellij.psi.PsiElement;
@@ -39,7 +40,11 @@
     }
   }
 
-  protected void markError(PsiElement element, String message) {
-    getHolder().createErrorAnnotation(element, message);
+  protected Annotation markError(PsiElement element, String message) {
+    return getHolder().createErrorAnnotation(element, message);
+  }
+
+  protected Annotation markWarning(PsiElement element, String message) {
+    return getHolder().createWarningAnnotation(element, message);
   }
 }
diff --git a/base/src/com/google/idea/blaze/base/lang/buildfile/validation/LoadErrorAnnotator.java b/base/src/com/google/idea/blaze/base/lang/buildfile/validation/LoadErrorAnnotator.java
deleted file mode 100644
index c8ea27a..0000000
--- a/base/src/com/google/idea/blaze/base/lang/buildfile/validation/LoadErrorAnnotator.java
+++ /dev/null
@@ -1,113 +0,0 @@
-/*
- * Copyright 2016 The Bazel Authors. All rights reserved.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *    http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.google.idea.blaze.base.lang.buildfile.validation;
-
-import com.google.idea.blaze.base.lang.buildfile.psi.Argument;
-import com.google.idea.blaze.base.lang.buildfile.psi.BuildElement;
-import com.google.idea.blaze.base.lang.buildfile.psi.BuildFile;
-import com.google.idea.blaze.base.lang.buildfile.psi.FuncallExpression;
-import com.google.idea.blaze.base.lang.buildfile.psi.FunctionStatement;
-import com.google.idea.blaze.base.lang.buildfile.psi.LoadStatement;
-import com.google.idea.blaze.base.lang.buildfile.psi.LoadedSymbol;
-import com.google.idea.blaze.base.lang.buildfile.psi.ParameterList;
-import com.google.idea.blaze.base.lang.buildfile.psi.StringLiteral;
-import com.google.idea.blaze.base.lang.buildfile.references.LabelReference;
-import com.intellij.psi.PsiElement;
-import java.util.Arrays;
-import javax.annotation.Nullable;
-
-/**
- * Error annotations for load statements, post parsing.
- *
- * <p>This has been turned off because it's unusable. BuildFile is re-parsed *every* time it's
- * touched, and is never cached. Until this is fixed, we can't run any annotators touching the file.
- *
- * <p>One option: try moving all expensive checks to 'visitFile', so they're not run in parallel
- */
-public class LoadErrorAnnotator extends BuildAnnotator {
-
-  @Override
-  public void visitLoadStatement(LoadStatement node) {
-    BuildElement[] children = node.buildElementChildren();
-    //    StringLiteral[] strings = node..getChildStrings();
-    if (children.length == 0) {
-      return;
-    }
-    PsiElement skylarkRef = getSkylarkRef(children[0]);
-    if (skylarkRef == null) {
-      markError(children[0], "Cannot find this Skylark module");
-      return;
-    }
-    if (!(skylarkRef instanceof BuildFile)) {
-      markError(children[0], children[0].getText() + " is not a Skylark module");
-      return;
-    }
-
-    LoadedSymbol[] symbols =
-        Arrays.stream(children)
-            .filter(element -> element instanceof LoadedSymbol)
-            .toArray(LoadedSymbol[]::new);
-    if (symbols.length == 1) {
-      markError(node, "No symbols imported from Skylark module");
-      return;
-    }
-    BuildFile skylarkModule = (BuildFile) skylarkRef;
-    for (int i = 0; i < symbols.length; i++) {
-      String text = symbols[i].getSymbolString();
-      if (text == null) {
-        continue;
-      }
-      FunctionStatement fn = skylarkModule.findDeclaredFunction(text);
-      if (fn == null) {
-        markError(
-            symbols[i],
-            "Function '" + text + "' not found in Skylark module " + skylarkModule.getFileName());
-      }
-    }
-  }
-
-  @Nullable
-  private static PsiElement getSkylarkRef(BuildElement firstChild) {
-    if (firstChild instanceof StringLiteral) {
-      return new LabelReference((StringLiteral) firstChild, false).resolve();
-    }
-    return null;
-  }
-
-  @Override
-  public void visitFuncallExpression(FuncallExpression node) {
-    FunctionStatement function = (FunctionStatement) node.getReferencedElement();
-    if (function == null) {
-      // likely a built-in rule.
-      return;
-    }
-    // check keyword args match function parameters
-    ParameterList params = function.getParameterList();
-    if (params == null || params.hasStarStar()) {
-      return;
-    }
-    for (Argument arg : node.getArguments()) {
-      if (arg instanceof Argument.Keyword) {
-        String name = arg.getName();
-        if (name != null && params.findParameterByName(name) == null) {
-          markError(
-              arg,
-              "No parameter found in '" + node.getFunctionName() + "' with name '" + name + "'");
-        }
-      }
-    }
-  }
-}
diff --git a/base/src/com/google/idea/blaze/base/lang/buildfile/validation/LoadStatementAnnotator.java b/base/src/com/google/idea/blaze/base/lang/buildfile/validation/LoadStatementAnnotator.java
new file mode 100644
index 0000000..b3c8a55
--- /dev/null
+++ b/base/src/com/google/idea/blaze/base/lang/buildfile/validation/LoadStatementAnnotator.java
@@ -0,0 +1,76 @@
+/*
+ * Copyright 2017 The Bazel Authors. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.idea.blaze.base.lang.buildfile.validation;
+
+import com.google.idea.blaze.base.lang.buildfile.psi.LoadStatement;
+import com.google.idea.blaze.base.lang.buildfile.psi.LoadedSymbol;
+import com.google.idea.blaze.base.lang.buildfile.psi.StringLiteral;
+import com.google.idea.blaze.base.lang.buildfile.quickfix.DeprecatedLoadQuickFix;
+import com.intellij.codeInspection.InspectionManager;
+import com.intellij.codeInspection.ProblemDescriptor;
+import com.intellij.codeInspection.ProblemHighlightType;
+import com.intellij.lang.annotation.Annotation;
+import javax.annotation.Nullable;
+
+/** Adds warning/error annotations to load statements. */
+public class LoadStatementAnnotator extends BuildAnnotator {
+
+  @Override
+  public void visitLoadStatement(LoadStatement node) {
+    validateImportTarget(node.getImportPsiElement());
+  }
+
+  @Override
+  public void visitLoadedSymbol(LoadedSymbol node) {
+    StringLiteral loadedString = node.getImport();
+    if (loadedString == null) {
+      return;
+    }
+    String name = loadedString.getStringContents();
+    if (name.startsWith("_")) {
+      markError(node, String.format("Symbol '%s' is private and cannot be imported.", name));
+    }
+  }
+
+  private void validateImportTarget(@Nullable StringLiteral target) {
+    if (target == null) {
+      return;
+    }
+    String targetString = target.getStringContents();
+    if (targetString == null
+        || targetString.startsWith(":")
+        || targetString.startsWith("//")
+        || targetString.length() < 2) {
+      return;
+    }
+    if (targetString.startsWith("/")) {
+      Annotation annotation =
+          markWarning(
+              target, "Deprecated load syntax; loaded Skylark module should by in label format.");
+      InspectionManager inspectionManager = InspectionManager.getInstance(target.getProject());
+      ProblemDescriptor descriptor =
+          inspectionManager.createProblemDescriptor(
+              target,
+              annotation.getMessage(),
+              DeprecatedLoadQuickFix.INSTANCE,
+              ProblemHighlightType.LIKE_DEPRECATED,
+              true);
+      annotation.registerFix(DeprecatedLoadQuickFix.INSTANCE, null, null, descriptor);
+      return;
+    }
+    markError(target, "Invalid load syntax: missing Skylark module.");
+  }
+}
diff --git a/base/src/com/google/idea/blaze/base/lang/buildfile/views/BuildStructureViewElement.java b/base/src/com/google/idea/blaze/base/lang/buildfile/views/BuildStructureViewElement.java
index 75768dd..7ae6217 100644
--- a/base/src/com/google/idea/blaze/base/lang/buildfile/views/BuildStructureViewElement.java
+++ b/base/src/com/google/idea/blaze/base/lang/buildfile/views/BuildStructureViewElement.java
@@ -21,8 +21,8 @@
 import com.intellij.ide.structureView.StructureViewTreeElement;
 import com.intellij.ide.structureView.impl.common.PsiTreeElementBase;
 import java.util.Collection;
+import javax.annotation.Nullable;
 import org.jetbrains.annotations.NotNull;
-import org.jetbrains.annotations.Nullable;
 
 /** Handles nodes in Structure View. */
 public class BuildStructureViewElement extends PsiTreeElementBase<BuildElement> {
diff --git a/base/src/com/google/idea/blaze/base/lang/projectview/completion/ProjectViewKeywordCompletionContributor.java b/base/src/com/google/idea/blaze/base/lang/projectview/completion/ProjectViewKeywordCompletionContributor.java
index 88d8d2b..6020f31 100644
--- a/base/src/com/google/idea/blaze/base/lang/projectview/completion/ProjectViewKeywordCompletionContributor.java
+++ b/base/src/com/google/idea/blaze/base/lang/projectview/completion/ProjectViewKeywordCompletionContributor.java
@@ -43,7 +43,7 @@
 import com.intellij.psi.PsiFile;
 import com.intellij.util.ProcessingContext;
 import java.util.List;
-import org.jetbrains.annotations.Nullable;
+import javax.annotation.Nullable;
 
 /** Completes project view section names. */
 public class ProjectViewKeywordCompletionContributor extends CompletionContributor {
diff --git a/base/src/com/google/idea/blaze/base/lang/projectview/formatting/ProjectViewCommenter.java b/base/src/com/google/idea/blaze/base/lang/projectview/formatting/ProjectViewCommenter.java
index 09aaa68..33667ee 100644
--- a/base/src/com/google/idea/blaze/base/lang/projectview/formatting/ProjectViewCommenter.java
+++ b/base/src/com/google/idea/blaze/base/lang/projectview/formatting/ProjectViewCommenter.java
@@ -19,7 +19,7 @@
 import com.intellij.lang.CodeDocumentationAwareCommenter;
 import com.intellij.psi.PsiComment;
 import com.intellij.psi.tree.IElementType;
-import org.jetbrains.annotations.Nullable;
+import javax.annotation.Nullable;
 
 /** Supports (un)commenting lines via IntelliJ */
 public class ProjectViewCommenter implements CodeDocumentationAwareCommenter {
diff --git a/base/src/com/google/idea/blaze/base/model/primitives/ExecutionRootPath.java b/base/src/com/google/idea/blaze/base/model/primitives/ExecutionRootPath.java
index 4c8d3f0..5e060b7 100644
--- a/base/src/com/google/idea/blaze/base/model/primitives/ExecutionRootPath.java
+++ b/base/src/com/google/idea/blaze/base/model/primitives/ExecutionRootPath.java
@@ -87,7 +87,7 @@
     if (root.isAbsolute() != path.isAbsolute()) {
       return null;
     }
-    if (!isAncestor(root.getPath(), path.getPath(), false /* strict */)) {
+    if (!isAncestor(root.getPath(), path.getPath(), /* strict */ false)) {
       return null;
     }
     String relativePath = FileUtil.getRelativePath(root, path);
diff --git a/base/src/com/google/idea/blaze/base/model/primitives/WorkspacePath.java b/base/src/com/google/idea/blaze/base/model/primitives/WorkspacePath.java
index 00ce9ee..b3cbbf0 100644
--- a/base/src/com/google/idea/blaze/base/model/primitives/WorkspacePath.java
+++ b/base/src/com/google/idea/blaze/base/model/primitives/WorkspacePath.java
@@ -18,9 +18,9 @@
 import com.google.idea.blaze.base.ui.BlazeValidationError;
 import java.io.Serializable;
 import java.util.Collection;
+import javax.annotation.Nullable;
 import javax.annotation.concurrent.Immutable;
 import org.jetbrains.annotations.NotNull;
-import org.jetbrains.annotations.Nullable;
 
 /**
  * Represents a path relative to the workspace root. The path component separator is Blaze specific.
diff --git a/base/src/com/google/idea/blaze/base/projectview/ProjectViewManagerImpl.java b/base/src/com/google/idea/blaze/base/projectview/ProjectViewManagerImpl.java
index bab57dc..1635b84 100644
--- a/base/src/com/google/idea/blaze/base/projectview/ProjectViewManagerImpl.java
+++ b/base/src/com/google/idea/blaze/base/projectview/ProjectViewManagerImpl.java
@@ -28,8 +28,8 @@
 import java.io.File;
 import java.io.IOException;
 import java.util.List;
+import javax.annotation.Nullable;
 import org.jetbrains.annotations.NotNull;
-import org.jetbrains.annotations.Nullable;
 
 /** Project view manager implementation. */
 /** Stores mutable per-project user settings. */
diff --git a/base/src/com/google/idea/blaze/base/projectview/ProjectViewStorageManagerImpl.java b/base/src/com/google/idea/blaze/base/projectview/ProjectViewStorageManagerImpl.java
index 729b1d8..49a5d3b 100644
--- a/base/src/com/google/idea/blaze/base/projectview/ProjectViewStorageManagerImpl.java
+++ b/base/src/com/google/idea/blaze/base/projectview/ProjectViewStorageManagerImpl.java
@@ -17,18 +17,16 @@
 
 import com.google.common.base.Charsets;
 import com.google.common.collect.ImmutableList;
-import com.intellij.openapi.diagnostic.Logger;
 import com.intellij.openapi.vfs.LocalFileSystem;
 import java.io.File;
 import java.io.FileInputStream;
 import java.io.FileWriter;
 import java.io.IOException;
+import javax.annotation.Nullable;
 import org.jetbrains.annotations.NotNull;
-import org.jetbrains.annotations.Nullable;
 
 /** Project view storage implementation. */
 final class ProjectViewStorageManagerImpl extends ProjectViewStorageManager {
-  private static final Logger logger = Logger.getInstance(ProjectViewManagerImpl.class);
 
   @Nullable
   @Override
diff --git a/base/src/com/google/idea/blaze/base/projectview/parser/ParseContext.java b/base/src/com/google/idea/blaze/base/projectview/parser/ParseContext.java
index a7b626a..29bacb8 100644
--- a/base/src/com/google/idea/blaze/base/projectview/parser/ParseContext.java
+++ b/base/src/com/google/idea/blaze/base/projectview/parser/ParseContext.java
@@ -22,7 +22,7 @@
 import com.google.idea.blaze.base.ui.BlazeValidationError;
 import java.io.File;
 import java.util.List;
-import org.jetbrains.annotations.Nullable;
+import javax.annotation.Nullable;
 
 /** Context for the project view parser. */
 public class ParseContext {
diff --git a/base/src/com/google/idea/blaze/base/projectview/section/sections/ImportSection.java b/base/src/com/google/idea/blaze/base/projectview/section/sections/ImportSection.java
index e377b09..4d9b6ef 100644
--- a/base/src/com/google/idea/blaze/base/projectview/section/sections/ImportSection.java
+++ b/base/src/com/google/idea/blaze/base/projectview/section/sections/ImportSection.java
@@ -26,7 +26,7 @@
 import com.google.idea.blaze.base.ui.BlazeValidationError;
 import java.io.File;
 import java.util.List;
-import org.jetbrains.annotations.Nullable;
+import javax.annotation.Nullable;
 
 /** "import" section. */
 public class ImportSection {
diff --git a/base/src/com/google/idea/blaze/base/run/BlazeCommandRunConfiguration.java b/base/src/com/google/idea/blaze/base/run/BlazeCommandRunConfiguration.java
index 9448702..7f59707 100644
--- a/base/src/com/google/idea/blaze/base/run/BlazeCommandRunConfiguration.java
+++ b/base/src/com/google/idea/blaze/base/run/BlazeCommandRunConfiguration.java
@@ -15,13 +15,18 @@
  */
 package com.google.idea.blaze.base.run;
 
+import static java.util.stream.Collectors.toList;
+
 import com.google.common.base.Strings;
-import com.google.common.collect.Lists;
+import com.google.common.collect.ImmutableList;
 import com.google.idea.blaze.base.ideinfo.TargetIdeInfo;
 import com.google.idea.blaze.base.model.BlazeProjectData;
 import com.google.idea.blaze.base.model.primitives.Kind;
 import com.google.idea.blaze.base.model.primitives.Label;
 import com.google.idea.blaze.base.model.primitives.TargetExpression;
+import com.google.idea.blaze.base.model.primitives.WorkspaceRoot;
+import com.google.idea.blaze.base.projectview.ProjectViewManager;
+import com.google.idea.blaze.base.projectview.ProjectViewSet;
 import com.google.idea.blaze.base.run.confighandler.BlazeCommandRunConfigurationHandler;
 import com.google.idea.blaze.base.run.confighandler.BlazeCommandRunConfigurationHandlerProvider;
 import com.google.idea.blaze.base.run.confighandler.BlazeCommandRunConfigurationRunner;
@@ -29,7 +34,10 @@
 import com.google.idea.blaze.base.run.state.RunConfigurationStateEditor;
 import com.google.idea.blaze.base.run.targetfinder.TargetFinder;
 import com.google.idea.blaze.base.settings.Blaze;
+import com.google.idea.blaze.base.settings.BlazeImportSettings;
+import com.google.idea.blaze.base.settings.BlazeImportSettingsManager;
 import com.google.idea.blaze.base.sync.data.BlazeProjectDataManager;
+import com.google.idea.blaze.base.sync.projectview.ImportRoots;
 import com.google.idea.blaze.base.ui.UiUtil;
 import com.intellij.execution.ExecutionException;
 import com.intellij.execution.Executor;
@@ -55,7 +63,6 @@
 import com.intellij.ui.components.JBLabel;
 import com.intellij.util.ui.UIUtil;
 import java.util.Collection;
-import java.util.List;
 import java.util.Set;
 import java.util.stream.Collectors;
 import javax.annotation.Nullable;
@@ -496,17 +503,27 @@
     }
 
     private static Collection<String> getTargets(Project project) {
-      List<String> result = Lists.newArrayList();
       BlazeProjectData projectData =
           BlazeProjectDataManager.getInstance(project).getBlazeProjectData();
-      if (projectData != null) {
-        for (TargetIdeInfo target : projectData.targetMap.targets()) {
-          if (target.isPlainTarget()) {
-            result.add(target.key.label.toString());
-          }
-        }
+      BlazeImportSettings importSettings =
+          BlazeImportSettingsManager.getInstance(project).getImportSettings();
+      ProjectViewSet projectViewSet = ProjectViewManager.getInstance(project).getProjectViewSet();
+      if (projectData == null || importSettings == null || projectViewSet == null) {
+        return ImmutableList.of();
       }
-      return result;
+      ImportRoots importRoots =
+          ImportRoots.builder(
+                  WorkspaceRoot.fromImportSettings(importSettings), importSettings.getBuildSystem())
+              .add(projectViewSet)
+              .build();
+      return projectData
+          .targetMap
+          .targets()
+          .stream()
+          .filter(TargetIdeInfo::isPlainTarget)
+          .filter(target -> importRoots.importAsSource(target.key.label))
+          .map(target -> target.key.label.toString())
+          .collect(toList());
     }
   }
 }
diff --git a/base/src/com/google/idea/blaze/base/run/BlazeRunConfiguration.java b/base/src/com/google/idea/blaze/base/run/BlazeRunConfiguration.java
index 5a4daed..3b98ced 100644
--- a/base/src/com/google/idea/blaze/base/run/BlazeRunConfiguration.java
+++ b/base/src/com/google/idea/blaze/base/run/BlazeRunConfiguration.java
@@ -16,7 +16,7 @@
 package com.google.idea.blaze.base.run;
 
 import com.google.idea.blaze.base.model.primitives.TargetExpression;
-import org.jetbrains.annotations.Nullable;
+import javax.annotation.Nullable;
 
 /** Marker interface for all run configurations */
 public interface BlazeRunConfiguration {
diff --git a/base/src/com/google/idea/blaze/base/run/BlazeRunConfigurationSyncListener.java b/base/src/com/google/idea/blaze/base/run/BlazeRunConfigurationSyncListener.java
index a1e0549..dfcc68a 100644
--- a/base/src/com/google/idea/blaze/base/run/BlazeRunConfigurationSyncListener.java
+++ b/base/src/com/google/idea/blaze/base/run/BlazeRunConfigurationSyncListener.java
@@ -119,7 +119,7 @@
       if (configurationFactory.handlesTarget(project, blazeProjectData, label)) {
         final RunnerAndConfigurationSettings settings =
             configurationFactory.createForTarget(project, runManager, label);
-        runManager.addConfiguration(settings, false /* isShared */);
+        runManager.addConfiguration(settings, /* isShared */ false);
         if (runManager.getSelectedConfiguration() == null) {
           // TODO(joshgiles): Better strategy for picking initially selected config.
           runManager.setSelectedConfiguration(settings);
diff --git a/base/src/com/google/idea/blaze/base/run/TargetNameHeuristic.java b/base/src/com/google/idea/blaze/base/run/TargetNameHeuristic.java
index c2cf40a..507fe2c 100644
--- a/base/src/com/google/idea/blaze/base/run/TargetNameHeuristic.java
+++ b/base/src/com/google/idea/blaze/base/run/TargetNameHeuristic.java
@@ -17,6 +17,7 @@
 
 import com.google.idea.blaze.base.ideinfo.TargetIdeInfo;
 import com.google.idea.blaze.base.ideinfo.TestIdeInfo.TestSize;
+import com.intellij.openapi.project.Project;
 import com.intellij.openapi.util.io.FileUtil;
 import java.io.File;
 import javax.annotation.Nullable;
@@ -25,7 +26,8 @@
 public class TargetNameHeuristic implements TestTargetHeuristic {
 
   @Override
-  public boolean matchesSource(TargetIdeInfo target, File sourceFile, @Nullable TestSize testSize) {
+  public boolean matchesSource(
+      Project project, TargetIdeInfo target, File sourceFile, @Nullable TestSize testSize) {
     String filePathWithoutExtension = FileUtil.getNameWithoutExtension(sourceFile.getPath());
     String targetName = target.key.label.targetName().toString();
     if (!filePathWithoutExtension.endsWith(targetName)) {
diff --git a/base/src/com/google/idea/blaze/base/run/TestSizeHeuristic.java b/base/src/com/google/idea/blaze/base/run/TestSizeHeuristic.java
index 67f899a..5b40bcf 100644
--- a/base/src/com/google/idea/blaze/base/run/TestSizeHeuristic.java
+++ b/base/src/com/google/idea/blaze/base/run/TestSizeHeuristic.java
@@ -18,6 +18,7 @@
 import com.google.idea.blaze.base.ideinfo.TargetIdeInfo;
 import com.google.idea.blaze.base.ideinfo.TestIdeInfo;
 import com.google.idea.blaze.base.ideinfo.TestIdeInfo.TestSize;
+import com.intellij.openapi.project.Project;
 import java.io.File;
 import javax.annotation.Nullable;
 
@@ -25,7 +26,8 @@
 public class TestSizeHeuristic implements TestTargetHeuristic {
 
   @Override
-  public boolean matchesSource(TargetIdeInfo target, File sourceFile, @Nullable TestSize testSize) {
+  public boolean matchesSource(
+      Project project, TargetIdeInfo target, File sourceFile, @Nullable TestSize testSize) {
     // If testSize == null then prefer small
     // Some test runners will assume no size annotation == small and filter on that, others will not
     TestSize size = testSize != null ? testSize : TestIdeInfo.DEFAULT_NON_ANNOTATED_TEST_SIZE;
diff --git a/base/src/com/google/idea/blaze/base/run/TestTargetHeuristic.java b/base/src/com/google/idea/blaze/base/run/TestTargetHeuristic.java
index 9f31b39..7d5576a 100644
--- a/base/src/com/google/idea/blaze/base/run/TestTargetHeuristic.java
+++ b/base/src/com/google/idea/blaze/base/run/TestTargetHeuristic.java
@@ -16,9 +16,10 @@
 package com.google.idea.blaze.base.run;
 
 import com.google.idea.blaze.base.ideinfo.TargetIdeInfo;
-import com.google.idea.blaze.base.ideinfo.TestIdeInfo;
+import com.google.idea.blaze.base.ideinfo.TestIdeInfo.TestSize;
 import com.google.idea.blaze.base.model.primitives.Label;
 import com.intellij.openapi.extensions.ExtensionPointName;
+import com.intellij.openapi.project.Project;
 import com.intellij.openapi.vfs.VirtualFile;
 import com.intellij.psi.PsiElement;
 import com.intellij.psi.PsiFile;
@@ -44,7 +45,7 @@
     }
     Collection<TargetIdeInfo> rules =
         TestTargetFinder.getInstance(element.getProject()).testTargetsForSourceFile(file);
-    return chooseTestTargetForSourceFile(file, rules, null);
+    return chooseTestTargetForSourceFile(element.getProject(), file, rules, null);
   }
 
   static File getContainingFile(PsiElement element) {
@@ -62,13 +63,16 @@
    */
   @Nullable
   static Label chooseTestTargetForSourceFile(
-      File sourceFile, Collection<TargetIdeInfo> targets, @Nullable TestIdeInfo.TestSize testSize) {
+      Project project,
+      File sourceFile,
+      Collection<TargetIdeInfo> targets,
+      @Nullable TestSize testSize) {
 
     for (TestTargetHeuristic filter : EP_NAME.getExtensions()) {
       TargetIdeInfo match =
           targets
               .stream()
-              .filter(target -> filter.matchesSource(target, sourceFile, testSize))
+              .filter(target -> filter.matchesSource(project, target, sourceFile, testSize))
               .findFirst()
               .orElse(null);
 
@@ -81,5 +85,5 @@
 
   /** Returns true if the rule and source file match, according to this heuristic. */
   boolean matchesSource(
-      TargetIdeInfo target, File sourceFile, @Nullable TestIdeInfo.TestSize testSize);
+      Project project, TargetIdeInfo target, File sourceFile, @Nullable TestSize testSize);
 }
diff --git a/base/src/com/google/idea/blaze/base/run/TestTargetSourcesHeuristic.java b/base/src/com/google/idea/blaze/base/run/TestTargetSourcesHeuristic.java
new file mode 100644
index 0000000..f42cf1b
--- /dev/null
+++ b/base/src/com/google/idea/blaze/base/run/TestTargetSourcesHeuristic.java
@@ -0,0 +1,50 @@
+/*
+ * Copyright 2017 The Bazel Authors. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.idea.blaze.base.run;
+
+import com.google.idea.blaze.base.ideinfo.ArtifactLocation;
+import com.google.idea.blaze.base.ideinfo.TargetIdeInfo;
+import com.google.idea.blaze.base.ideinfo.TestIdeInfo.TestSize;
+import com.google.idea.blaze.base.model.BlazeProjectData;
+import com.google.idea.blaze.base.sync.data.BlazeProjectDataManager;
+import com.google.idea.blaze.base.sync.workspace.ArtifactLocationDecoder;
+import com.intellij.openapi.project.Project;
+import java.io.File;
+import javax.annotation.Nullable;
+
+/**
+ * Matches source files to test targets, if the source file is present in the test target's 'srcs'
+ * list. Only looks for exact matches.
+ */
+public class TestTargetSourcesHeuristic implements TestTargetHeuristic {
+
+  @Override
+  public boolean matchesSource(
+      Project project, TargetIdeInfo target, File sourceFile, @Nullable TestSize testSize) {
+    BlazeProjectData projectData =
+        BlazeProjectDataManager.getInstance(project).getBlazeProjectData();
+    if (projectData == null) {
+      return false;
+    }
+    ArtifactLocationDecoder decoder = projectData.artifactLocationDecoder;
+    for (ArtifactLocation src : target.sources) {
+      if (decoder.decode(src).equals(sourceFile)) {
+        return true;
+      }
+    }
+    return false;
+  }
+}
diff --git a/base/src/com/google/idea/blaze/base/run/confighandler/BlazeCommandGenericRunConfigurationRunner.java b/base/src/com/google/idea/blaze/base/run/confighandler/BlazeCommandGenericRunConfigurationRunner.java
index 7c508e2..2c6bd90 100644
--- a/base/src/com/google/idea/blaze/base/run/confighandler/BlazeCommandGenericRunConfigurationRunner.java
+++ b/base/src/com/google/idea/blaze/base/run/confighandler/BlazeCommandGenericRunConfigurationRunner.java
@@ -38,7 +38,6 @@
 import com.google.idea.blaze.base.settings.Blaze;
 import com.google.idea.blaze.base.settings.BlazeImportSettings;
 import com.google.idea.blaze.base.settings.BlazeImportSettingsManager;
-import com.google.idea.common.experiments.BoolExperiment;
 import com.intellij.execution.DefaultExecutionResult;
 import com.intellij.execution.ExecutionException;
 import com.intellij.execution.ExecutionResult;
@@ -65,9 +64,6 @@
 public final class BlazeCommandGenericRunConfigurationRunner
     implements BlazeCommandRunConfigurationRunner {
 
-  private static final BoolExperiment smRunnerUiEnabled =
-      new BoolExperiment("use.smrunner.ui.general", true);
-
   @Override
   public RunProfileState getRunProfileState(Executor executor, ExecutionEnvironment environment) {
     return new BlazeCommandRunProfileState(environment, ImmutableList.of());
@@ -152,9 +148,7 @@
           new ScopedBlazeProcessHandler.ScopedProcessHandlerDelegate() {
             @Override
             public void onBlazeContextStart(BlazeContext context) {
-              context
-                  .push(new IssuesScope(project))
-                  .push(new IdeaLogScope());
+              context.push(new IssuesScope(project)).push(new IdeaLogScope());
             }
 
             @Override
@@ -171,28 +165,27 @@
       ProjectViewSet projectViewSet = ProjectViewManager.getInstance(project).getProjectViewSet();
       assert projectViewSet != null;
 
+      String binaryPath =
+          handlerState.getBlazeBinary() != null
+              ? handlerState.getBlazeBinary()
+              : Blaze.getBuildSystemProvider(project).getBinaryPath();
+
       BlazeCommand.Builder command =
-          BlazeCommand.builder(Blaze.getBuildSystem(project), handlerState.getCommand())
-              .setBlazeBinary(handlerState.getBlazeBinary())
+          BlazeCommand.builder(binaryPath, handlerState.getCommand())
               .addTargets(configuration.getTarget())
               .addBlazeFlags(BlazeFlags.buildFlags(project, projectViewSet))
               .addBlazeFlags(testHandlerFlags)
               .addBlazeFlags(handlerState.getBlazeFlags())
               .addExeFlags(handlerState.getExeFlags());
 
-      boolean runDistributed = handlerState.getRunOnDistributedExecutor();
       command.addBlazeFlags(
           DistributedExecutorSupport.getBlazeFlags(
               project, handlerState.getRunOnDistributedExecutor()));
-      if (!runDistributed) {
-        command.addBlazeFlags(BlazeFlags.TEST_OUTPUT_STREAMED);
-      }
       return command.build();
     }
 
     private boolean canUseTestUi() {
-      return smRunnerUiEnabled.getValue()
-          && BlazeCommandName.TEST.equals(handlerState.getCommand())
+      return BlazeCommandName.TEST.equals(handlerState.getCommand())
           && !handlerState.getRunOnDistributedExecutor();
     }
   }
diff --git a/base/src/com/google/idea/blaze/base/run/filter/BlazeTargetFilter.java b/base/src/com/google/idea/blaze/base/run/filter/BlazeTargetFilter.java
index 1cf0e0c..07e6233 100644
--- a/base/src/com/google/idea/blaze/base/run/filter/BlazeTargetFilter.java
+++ b/base/src/com/google/idea/blaze/base/run/filter/BlazeTargetFilter.java
@@ -25,7 +25,7 @@
 import com.intellij.psi.PsiElement;
 import java.util.regex.Matcher;
 import java.util.regex.Pattern;
-import org.jetbrains.annotations.Nullable;
+import javax.annotation.Nullable;
 
 /** Parse blaze targets in streamed output. */
 public class BlazeTargetFilter implements Filter {
diff --git a/base/src/com/google/idea/blaze/base/run/producers/AllInPackageBlazeConfigurationProducer.java b/base/src/com/google/idea/blaze/base/run/producers/AllInPackageBlazeConfigurationProducer.java
index 154a9be..372a315 100644
--- a/base/src/com/google/idea/blaze/base/run/producers/AllInPackageBlazeConfigurationProducer.java
+++ b/base/src/com/google/idea/blaze/base/run/producers/AllInPackageBlazeConfigurationProducer.java
@@ -16,6 +16,7 @@
 package com.google.idea.blaze.base.run.producers;
 
 import com.google.idea.blaze.base.command.BlazeCommandName;
+import com.google.idea.blaze.base.lang.buildfile.search.BlazePackage;
 import com.google.idea.blaze.base.model.primitives.TargetExpression;
 import com.google.idea.blaze.base.model.primitives.WorkspacePath;
 import com.google.idea.blaze.base.model.primitives.WorkspaceRoot;
@@ -93,13 +94,11 @@
   private static PsiDirectory getTestDirectory(ConfigurationContext context) {
     WorkspaceRoot root = WorkspaceRoot.fromProject(context.getModule().getProject());
     PsiElement location = context.getPsiLocation();
-    if (location instanceof PsiDirectory) {
-      PsiDirectory dir = (PsiDirectory) location;
-      if (isInWorkspace(root, dir)) {
-        return dir;
-      }
+    if (!(location instanceof PsiDirectory)) {
+      return null;
     }
-    return null;
+    PsiDirectory dir = (PsiDirectory) location;
+    return isInWorkspace(root, dir) && BlazePackage.hasBlazePackageChild(dir) ? dir : null;
   }
 
   @Nullable
diff --git a/base/src/com/google/idea/blaze/base/run/smrunner/BlazeCompositeTestEventsHandler.java b/base/src/com/google/idea/blaze/base/run/smrunner/BlazeCompositeTestEventsHandler.java
index 5d86bdf..21bf76a 100644
--- a/base/src/com/google/idea/blaze/base/run/smrunner/BlazeCompositeTestEventsHandler.java
+++ b/base/src/com/google/idea/blaze/base/run/smrunner/BlazeCompositeTestEventsHandler.java
@@ -20,6 +20,7 @@
 import com.google.common.collect.ImmutableSet;
 import com.google.common.collect.Maps;
 import com.google.idea.blaze.base.model.primitives.Kind;
+import com.google.idea.blaze.base.run.smrunner.BlazeXmlSchema.TestSuite;
 import com.intellij.execution.Location;
 import com.intellij.execution.testframework.actions.AbstractRerunFailedTestsAction;
 import com.intellij.execution.testframework.sm.runner.SMTestLocator;
@@ -95,6 +96,12 @@
     return null;
   }
 
+  @Override
+  public boolean ignoreSuite(@Nullable Kind kind, TestSuite suite) {
+    BlazeTestEventsHandler handler = kind != null ? getHandlers().get(kind) : null;
+    return handler != null ? handler.ignoreSuite(kind, suite) : super.ignoreSuite(kind, suite);
+  }
+
   /** Converts the testsuite name in the blaze test XML to a user-friendly format */
   @Override
   public String suiteDisplayName(@Nullable Kind kind, String rawName) {
diff --git a/base/src/com/google/idea/blaze/base/run/smrunner/BlazeTestEventsHandler.java b/base/src/com/google/idea/blaze/base/run/smrunner/BlazeTestEventsHandler.java
index 2f3286f..7bb4325 100644
--- a/base/src/com/google/idea/blaze/base/run/smrunner/BlazeTestEventsHandler.java
+++ b/base/src/com/google/idea/blaze/base/run/smrunner/BlazeTestEventsHandler.java
@@ -44,18 +44,17 @@
 
   /**
    * Blaze/Bazel flags required for test UI.<br>
-   * Forces local test execution, without sharding.
+   * Forces local test execution, without retries.
    */
   public static ImmutableList<String> getBlazeFlags(Project project) {
     ImmutableList.Builder<String> flags =
-        ImmutableList.<String>builder()
-            .add(
-                "--test_sharding_strategy=disabled",
-                "--runs_per_test=1",
-                "--flaky_test_attempts=1");
+        ImmutableList.<String>builder().add("--runs_per_test=1", "--flaky_test_attempts=1");
     if (Blaze.getBuildSystem(project) == BuildSystem.Blaze) {
       flags.add("--test_strategy=local");
     }
+    if (Blaze.getBuildSystem(project) == BuildSystem.Bazel) {
+      flags.add("--test_sharding_strategy=disabled");
+    }
     return flags.build();
   }
 
@@ -125,8 +124,8 @@
   }
 
   /** Whether to skip logging a {@link TestSuite}. */
-  public boolean ignoreSuite(TestSuite suite) {
+  public boolean ignoreSuite(@Nullable Kind kind, TestSuite suite) {
     // by default only include innermost 'testsuite' elements
-    return suite.testSuites.isEmpty();
+    return !suite.testSuites.isEmpty();
   }
 }
diff --git a/base/src/com/google/idea/blaze/base/run/smrunner/BlazeXmlSchema.java b/base/src/com/google/idea/blaze/base/run/smrunner/BlazeXmlSchema.java
index 4126bc8..f1f053a 100644
--- a/base/src/com/google/idea/blaze/base/run/smrunner/BlazeXmlSchema.java
+++ b/base/src/com/google/idea/blaze/base/run/smrunner/BlazeXmlSchema.java
@@ -18,6 +18,7 @@
 import com.google.common.collect.Lists;
 import java.io.InputStream;
 import java.util.List;
+import java.util.Objects;
 import javax.xml.bind.JAXBContext;
 import javax.xml.bind.JAXBException;
 import javax.xml.bind.annotation.XmlAttribute;
@@ -95,6 +96,40 @@
 
     @XmlElement(name = "testcase")
     List<TestCase> testCases = Lists.newArrayList();
+
+    /** Used to merge test suites from a single target, split across multiple shards */
+    private void addSuite(TestSuite suite) {
+      for (TestSuite existing : testSuites) {
+        if (Objects.equals(existing.name, suite.name)) {
+          existing.mergeWithSuite(suite);
+          return;
+        }
+      }
+      testSuites.add(suite);
+    }
+
+    private void mergeWithSuite(TestSuite suite) {
+      for (TestSuite child : suite.testSuites) {
+        addSuite(child);
+      }
+      testDecorators.addAll(suite.testDecorators);
+      testCases.addAll(suite.testCases);
+      tests += suite.tests;
+      failures += suite.failures;
+      errors += suite.errors;
+      skipped += suite.skipped;
+      disabled += suite.disabled;
+      time += suite.time;
+    }
+  }
+
+  /** Used to merge test suites from a single target, split across multiple shards */
+  static TestSuite mergeSuites(List<TestSuite> suites) {
+    TestSuite outer = new TestSuite();
+    for (TestSuite suite : suites) {
+      outer.addSuite(suite);
+    }
+    return outer;
   }
 
   static class TestCase {
diff --git a/base/src/com/google/idea/blaze/base/run/smrunner/BlazeXmlToTestEventsConverter.java b/base/src/com/google/idea/blaze/base/run/smrunner/BlazeXmlToTestEventsConverter.java
index 548557a..be9d154 100644
--- a/base/src/com/google/idea/blaze/base/run/smrunner/BlazeXmlToTestEventsConverter.java
+++ b/base/src/com/google/idea/blaze/base/run/smrunner/BlazeXmlToTestEventsConverter.java
@@ -15,6 +15,7 @@
  */
 package com.google.idea.blaze.base.run.smrunner;
 
+import com.google.common.collect.ImmutableMultimap;
 import com.google.idea.blaze.base.ideinfo.TargetIdeInfo;
 import com.google.idea.blaze.base.model.primitives.Kind;
 import com.google.idea.blaze.base.model.primitives.Label;
@@ -23,7 +24,6 @@
 import com.google.idea.blaze.base.run.smrunner.BlazeXmlSchema.TestSuite;
 import com.google.idea.blaze.base.run.targetfinder.TargetFinder;
 import com.google.idea.blaze.base.run.testlogs.BlazeTestXmlFinderStrategy;
-import com.google.idea.blaze.base.run.testlogs.CompletedTestTarget;
 import com.google.idea.sdkcompat.smrunner.SmRunnerCompatUtils;
 import com.intellij.execution.process.ProcessOutputTypes;
 import com.intellij.execution.testframework.TestConsoleProperties;
@@ -37,9 +37,13 @@
 import com.intellij.execution.testframework.sm.runner.events.TestSuiteStartedEvent;
 import com.intellij.openapi.project.Project;
 import com.intellij.openapi.util.Key;
+import java.io.File;
 import java.io.FileInputStream;
 import java.io.InputStream;
 import java.text.ParseException;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
 import javax.annotation.Nullable;
 import jetbrains.buildServer.messages.serviceMessages.ServiceMessageVisitor;
 import jetbrains.buildServer.messages.serviceMessages.TestSuiteStarted;
@@ -87,14 +91,31 @@
     onStartTesting();
     getProcessor().onTestsReporterAttached();
 
-    for (CompletedTestTarget testTarget : BlazeTestXmlFinderStrategy.locateTestXmlFiles(project)) {
-      try (InputStream input = new FileInputStream(testTarget.testResultXml)) {
-        parseXmlInput(getProcessor(), getKind(project, testTarget.label), input);
+    ImmutableMultimap<Label, File> xmlFiles =
+        BlazeTestXmlFinderStrategy.locateTestXmlFiles(project);
+    for (Label label : xmlFiles.keySet()) {
+      processTestSuites(label, xmlFiles.get(label));
+    }
+  }
+
+  /** Process all test XML files from a single test target. */
+  private void processTestSuites(Label label, Collection<File> files) {
+    Kind kind = getKind(project, label);
+    List<TestSuite> targetSuites = new ArrayList<>();
+    for (File file : files) {
+      try (InputStream input = new FileInputStream(file)) {
+        targetSuites.add(BlazeXmlSchema.parse(input));
       } catch (Exception e) {
         // ignore parsing errors -- most common cause is user cancellation, which we can't easily
         // recognize.
       }
     }
+    if (targetSuites.isEmpty()) {
+      return;
+    }
+    TestSuite suite =
+        targetSuites.size() == 1 ? targetSuites.get(0) : BlazeXmlSchema.mergeSuites(targetSuites);
+    processTestSuite(getProcessor(), kind, suite);
   }
 
   @Nullable
@@ -103,19 +124,13 @@
     return target != null ? target.kind : null;
   }
 
-  private void parseXmlInput(
-      GeneralTestEventsProcessor processor, @Nullable Kind kind, InputStream input) {
-    TestSuite testResult = BlazeXmlSchema.parse(input);
-    processTestSuite(processor, kind, testResult);
-  }
-
   private void processTestSuite(
       GeneralTestEventsProcessor processor, @Nullable Kind kind, TestSuite suite) {
     if (!hasRunChild(suite)) {
       return;
     }
     // only include the innermost 'testsuite' element
-    boolean logSuite = !eventsHandler.ignoreSuite(suite);
+    boolean logSuite = !eventsHandler.ignoreSuite(kind, suite);
     if (suite.name != null && logSuite) {
       TestSuiteStarted suiteStarted =
           new TestSuiteStarted(eventsHandler.suiteDisplayName(kind, suite.name));
diff --git a/base/src/com/google/idea/blaze/base/run/testlogs/BlazeTestXmlFinderStrategy.java b/base/src/com/google/idea/blaze/base/run/testlogs/BlazeTestXmlFinderStrategy.java
index 174d512..a069108 100644
--- a/base/src/com/google/idea/blaze/base/run/testlogs/BlazeTestXmlFinderStrategy.java
+++ b/base/src/com/google/idea/blaze/base/run/testlogs/BlazeTestXmlFinderStrategy.java
@@ -15,11 +15,13 @@
  */
 package com.google.idea.blaze.base.run.testlogs;
 
-import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMultimap;
+import com.google.idea.blaze.base.model.primitives.Label;
 import com.google.idea.blaze.base.settings.Blaze;
 import com.google.idea.blaze.base.settings.Blaze.BuildSystem;
 import com.intellij.openapi.extensions.ExtensionPointName;
 import com.intellij.openapi.project.Project;
+import java.io.File;
 
 /** A strategy for locating output test XML files. */
 public interface BlazeTestXmlFinderStrategy {
@@ -28,25 +30,25 @@
       ExtensionPointName.create("com.google.idea.blaze.BlazeTestXmlFinderStrategy");
 
   /**
-   * Attempt to find all output test XML files associated with the given run configuration. Called
-   * after the 'blaze test' process completes.
+   * Attempt to find all output test XML files produced by the most recent blaze invocation, grouped
+   * by target label.
    */
-  static ImmutableList<CompletedTestTarget> locateTestXmlFiles(Project project) {
+  static ImmutableMultimap<Label, File> locateTestXmlFiles(Project project) {
     BuildSystem buildSystem = Blaze.getBuildSystem(project);
-    ImmutableList.Builder<CompletedTestTarget> output = ImmutableList.builder();
+    ImmutableMultimap.Builder<Label, File> output = ImmutableMultimap.builder();
     for (BlazeTestXmlFinderStrategy strategy : EP_NAME.getExtensions()) {
       if (strategy.handlesBuildSystem(buildSystem)) {
-        output.addAll(strategy.findTestXmlFiles(project));
+        output.putAll(strategy.findTestXmlFiles(project));
       }
     }
     return output.build();
   }
 
   /**
-   * Attempt to find all output test XML files associated with the given run configuration using a
-   * particular strategy. Called after the 'blaze test' process completes.
+   * Attempt to find all output test XML files produced by the most recent blaze invocation, grouped
+   * by target label. Called after the 'blaze test' process completes.
    */
-  ImmutableList<CompletedTestTarget> findTestXmlFiles(Project project);
+  ImmutableMultimap<Label, File> findTestXmlFiles(Project project);
 
   boolean handlesBuildSystem(BuildSystem buildSystem);
 }
diff --git a/base/src/com/google/idea/blaze/base/run/testlogs/TargetPathTestXmlFinderStrategy.java b/base/src/com/google/idea/blaze/base/run/testlogs/TargetPathTestXmlFinderStrategy.java
index ce5ec40..30b2b05 100644
--- a/base/src/com/google/idea/blaze/base/run/testlogs/TargetPathTestXmlFinderStrategy.java
+++ b/base/src/com/google/idea/blaze/base/run/testlogs/TargetPathTestXmlFinderStrategy.java
@@ -15,7 +15,7 @@
  */
 package com.google.idea.blaze.base.run.testlogs;
 
-import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMultimap;
 import com.google.idea.blaze.base.command.info.BlazeInfo;
 import com.google.idea.blaze.base.model.BlazeProjectData;
 import com.google.idea.blaze.base.model.primitives.Label;
@@ -24,8 +24,6 @@
 import com.google.idea.blaze.base.sync.data.BlazeProjectDataManager;
 import com.intellij.openapi.project.Project;
 import java.io.File;
-import java.util.Objects;
-import java.util.stream.Collectors;
 import javax.annotation.Nullable;
 
 /**
@@ -40,28 +38,29 @@
   }
 
   @Override
-  public ImmutableList<CompletedTestTarget> findTestXmlFiles(Project project) {
+  public ImmutableMultimap<Label, File> findTestXmlFiles(Project project) {
     File testLogsDir = getTestLogsTree(project);
     if (testLogsDir == null) {
-      return ImmutableList.of();
+      return ImmutableMultimap.of();
     }
     File commandLog = getCommandLog(project);
     if (commandLog == null) {
-      return ImmutableList.of();
+      return ImmutableMultimap.of();
     }
-    return ImmutableList.copyOf(
-        BlazeCommandLogParser.parseTestTargets(commandLog)
-            .stream()
-            .map((label) -> toKindAndTestXml(testLogsDir, label))
-            .filter(Objects::nonNull)
-            .collect(Collectors.toList()));
+    ImmutableMultimap.Builder<Label, File> output = ImmutableMultimap.builder();
+    for (Label label : BlazeCommandLogParser.parseTestTargets(commandLog)) {
+      File testXml = findTestXml(testLogsDir, label);
+      if (testXml != null) {
+        output.put(label, testXml);
+      }
+    }
+    return output.build();
   }
 
   @Nullable
-  private static CompletedTestTarget toKindAndTestXml(File testLogsDir, Label label) {
+  private static File findTestXml(File testLogsDir, Label label) {
     String labelPath = label.blazePackage() + File.separator + label.targetName();
-    File testXml = new File(testLogsDir, labelPath + File.separator + "test.xml");
-    return new CompletedTestTarget(testXml, label);
+    return new File(testLogsDir, labelPath + File.separator + "test.xml");
   }
 
   @Nullable
diff --git a/base/src/com/google/idea/blaze/base/scope/BlazeContext.java b/base/src/com/google/idea/blaze/base/scope/BlazeContext.java
index 316c05b..f502c10 100644
--- a/base/src/com/google/idea/blaze/base/scope/BlazeContext.java
+++ b/base/src/com/google/idea/blaze/base/scope/BlazeContext.java
@@ -20,8 +20,8 @@
 import com.google.common.collect.ListMultimap;
 import com.google.common.collect.Lists;
 import java.util.List;
+import javax.annotation.Nullable;
 import org.jetbrains.annotations.NotNull;
-import org.jetbrains.annotations.Nullable;
 
 /** Scoped operation context. */
 public class BlazeContext {
diff --git a/base/src/com/google/idea/blaze/base/scope/Scope.java b/base/src/com/google/idea/blaze/base/scope/Scope.java
index 4c0f12a..27003e5 100644
--- a/base/src/com/google/idea/blaze/base/scope/Scope.java
+++ b/base/src/com/google/idea/blaze/base/scope/Scope.java
@@ -16,8 +16,8 @@
 package com.google.idea.blaze.base.scope;
 
 import com.intellij.openapi.diagnostic.Logger;
+import javax.annotation.Nullable;
 import org.jetbrains.annotations.NotNull;
-import org.jetbrains.annotations.Nullable;
 
 /** Helper methods to run scoped functions and operations in a scoped context. */
 public final class Scope {
diff --git a/base/src/com/google/idea/blaze/base/scope/ScopedTask.java b/base/src/com/google/idea/blaze/base/scope/ScopedTask.java
index 725bf2f..877cbe8 100644
--- a/base/src/com/google/idea/blaze/base/scope/ScopedTask.java
+++ b/base/src/com/google/idea/blaze/base/scope/ScopedTask.java
@@ -18,15 +18,15 @@
 import com.google.idea.blaze.base.scope.scopes.ProgressIndicatorScope;
 import com.intellij.openapi.progress.ProgressIndicator;
 import com.intellij.openapi.progress.Progressive;
+import javax.annotation.Nullable;
 import org.jetbrains.annotations.NotNull;
-import org.jetbrains.annotations.Nullable;
 
 /** Wrapper between an IntelliJ Task and a BlazeContext */
 public abstract class ScopedTask implements Progressive {
   @Nullable final BlazeContext parentContext;
 
   public ScopedTask() {
-    this(null /* parentContext */);
+    this(/* parentContext */ null);
   }
 
   public ScopedTask(@Nullable BlazeContext parentContext) {
diff --git a/base/src/com/google/idea/blaze/base/scope/output/IssueOutput.java b/base/src/com/google/idea/blaze/base/scope/output/IssueOutput.java
index e3bc15a..8690be2 100644
--- a/base/src/com/google/idea/blaze/base/scope/output/IssueOutput.java
+++ b/base/src/com/google/idea/blaze/base/scope/output/IssueOutput.java
@@ -19,8 +19,8 @@
 import com.google.idea.blaze.base.scope.Output;
 import com.intellij.pom.Navigatable;
 import java.io.File;
+import javax.annotation.Nullable;
 import org.jetbrains.annotations.NotNull;
-import org.jetbrains.annotations.Nullable;
 
 /** An issue in a blaze operation. */
 public class IssueOutput implements Output {
diff --git a/base/src/com/google/idea/blaze/base/scope/scopes/BlazeConsoleScope.java b/base/src/com/google/idea/blaze/base/scope/scopes/BlazeConsoleScope.java
index 1875f20..7128856 100644
--- a/base/src/com/google/idea/blaze/base/scope/scopes/BlazeConsoleScope.java
+++ b/base/src/com/google/idea/blaze/base/scope/scopes/BlazeConsoleScope.java
@@ -28,8 +28,8 @@
 import com.intellij.openapi.application.ApplicationManager;
 import com.intellij.openapi.progress.ProgressIndicator;
 import com.intellij.openapi.project.Project;
+import javax.annotation.Nullable;
 import org.jetbrains.annotations.NotNull;
-import org.jetbrains.annotations.Nullable;
 
 /** Moves print output to the blaze console. */
 public class BlazeConsoleScope implements BlazeScope {
diff --git a/base/src/com/google/idea/blaze/base/scope/scopes/TimingScope.java b/base/src/com/google/idea/blaze/base/scope/scopes/TimingScope.java
index d3dcad0..bad8849 100644
--- a/base/src/com/google/idea/blaze/base/scope/scopes/TimingScope.java
+++ b/base/src/com/google/idea/blaze/base/scope/scopes/TimingScope.java
@@ -20,8 +20,8 @@
 import com.google.idea.blaze.base.scope.BlazeScope;
 import com.google.idea.blaze.base.scope.output.PrintOutput;
 import java.util.List;
+import javax.annotation.Nullable;
 import org.jetbrains.annotations.NotNull;
-import org.jetbrains.annotations.Nullable;
 
 /** Prints timing information as output. */
 public class TimingScope implements BlazeScope {
diff --git a/base/src/com/google/idea/blaze/base/settings/BlazeImportSettings.java b/base/src/com/google/idea/blaze/base/settings/BlazeImportSettings.java
index 7f0ea60..86194e8 100644
--- a/base/src/com/google/idea/blaze/base/settings/BlazeImportSettings.java
+++ b/base/src/com/google/idea/blaze/base/settings/BlazeImportSettings.java
@@ -16,13 +16,9 @@
 package com.google.idea.blaze.base.settings;
 
 import com.google.idea.blaze.base.settings.Blaze.BuildSystem;
-import com.intellij.util.xmlb.annotations.Tag;
 import javax.annotation.Nullable;
 
-// The tag here is for legacy migration support.
-// No longer needed and can be removed with {@link BlazeImportSettingsManagerLegacy}
 /** Project settings that are set at import time. */
-@Tag("BlazeProjectSettings")
 public final class BlazeImportSettings {
 
   private String workspaceRoot = "";
diff --git a/base/src/com/google/idea/blaze/base/settings/BlazeImportSettingsManager.java b/base/src/com/google/idea/blaze/base/settings/BlazeImportSettingsManager.java
index a45b08e..093b18a 100644
--- a/base/src/com/google/idea/blaze/base/settings/BlazeImportSettingsManager.java
+++ b/base/src/com/google/idea/blaze/base/settings/BlazeImportSettingsManager.java
@@ -21,8 +21,8 @@
 import com.intellij.openapi.components.Storage;
 import com.intellij.openapi.components.StoragePathMacros;
 import com.intellij.openapi.project.Project;
+import javax.annotation.Nullable;
 import org.jetbrains.annotations.NotNull;
-import org.jetbrains.annotations.Nullable;
 
 /** Manages storage for the project's {@link BlazeImportSettings}. */
 @State(name = "BlazeImportSettings", storages = @Storage(file = StoragePathMacros.WORKSPACE_FILE))
@@ -30,11 +30,7 @@
 
   @Nullable private BlazeImportSettings importSettings;
 
-  private Project project;
-
-  public BlazeImportSettingsManager(@NotNull Project project) {
-    this.project = project;
-  }
+  public BlazeImportSettingsManager() {}
 
   public static BlazeImportSettingsManager getInstance(Project project) {
     return ServiceManager.getService(project, BlazeImportSettingsManager.class);
@@ -54,11 +50,6 @@
 
   @Nullable
   public BlazeImportSettings getImportSettings() {
-    if (importSettings == null) {
-      importSettings =
-          BlazeImportSettingsManagerLegacy.getInstance(project).migrateImportSettings();
-    }
-
     return importSettings;
   }
 
diff --git a/base/src/com/google/idea/blaze/base/settings/BlazeImportSettingsManagerLegacy.java b/base/src/com/google/idea/blaze/base/settings/BlazeImportSettingsManagerLegacy.java
deleted file mode 100644
index fec7d1d..0000000
--- a/base/src/com/google/idea/blaze/base/settings/BlazeImportSettingsManagerLegacy.java
+++ /dev/null
@@ -1,115 +0,0 @@
-/*
- * Copyright 2016 The Bazel Authors. All rights reserved.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.google.idea.blaze.base.settings;
-
-import com.google.common.collect.Lists;
-import com.intellij.openapi.components.PersistentStateComponent;
-import com.intellij.openapi.components.ServiceManager;
-import com.intellij.openapi.components.State;
-import com.intellij.openapi.components.Storage;
-import com.intellij.openapi.components.StoragePathMacros;
-import com.intellij.openapi.components.StorageScheme;
-import com.intellij.openapi.project.Project;
-import com.intellij.util.xmlb.annotations.AbstractCollection;
-import java.util.Collection;
-import java.util.List;
-import org.jetbrains.annotations.NotNull;
-import org.jetbrains.annotations.Nullable;
-
-/**
- * Legacy storage for BlazeImportSettings. Introduced in 1.8, can be removed around ~2.2. Removal of
- * this class will cause old projects to stop loading.
- */
-@State(
-  name = "BlazeSettings",
-  storages = {
-    @Storage(file = StoragePathMacros.PROJECT_FILE),
-    @Storage(
-      file = StoragePathMacros.PROJECT_CONFIG_DIR + "/blaze.xml",
-      scheme = StorageScheme.DIRECTORY_BASED
-    )
-  }
-)
-public class BlazeImportSettingsManagerLegacy
-    implements PersistentStateComponent<BlazeImportSettingsManagerLegacy.State> {
-
-  @Nullable private BlazeImportSettings importSettings;
-
-  @NotNull private Project project;
-
-  public BlazeImportSettingsManagerLegacy(@NotNull Project project) {
-    this.project = project;
-  }
-
-  @NotNull
-  public static BlazeImportSettingsManagerLegacy getInstance(@NotNull Project project) {
-    return ServiceManager.getService(project, BlazeImportSettingsManagerLegacy.class);
-  }
-
-  @SuppressWarnings("unchecked")
-  @Nullable
-  @Override
-  public State getState() {
-    if (importSettings == null) {
-      return null;
-    }
-    State state = new State();
-    List<BlazeImportSettings> value = Lists.newArrayList();
-    value.add(importSettings);
-    state.setLinkedExternalProjectsSettings(value);
-    return state;
-  }
-
-  @Override
-  public void loadState(State state) {
-    if (state == null) {
-      importSettings = null;
-      return;
-    }
-
-    Collection<BlazeImportSettings> settings = state.getLinkedExternalProjectsSettings();
-    if (settings != null && !settings.isEmpty()) {
-      importSettings = settings.iterator().next();
-    } else {
-      importSettings = null;
-    }
-  }
-
-  @Nullable
-  BlazeImportSettings migrateImportSettings() {
-    BlazeImportSettings importSettings = this.importSettings;
-    this.importSettings = null;
-    return importSettings;
-  }
-
-  /** State class for the Blaze settings. */
-  static class State {
-
-    private List<BlazeImportSettings> importSettings = Lists.newArrayList();
-
-    @AbstractCollection(
-      surroundWithTag = false,
-      elementTypes = {BlazeImportSettings.class}
-    )
-    public List<BlazeImportSettings> getLinkedExternalProjectsSettings() {
-      return importSettings;
-    }
-
-    public void setLinkedExternalProjectsSettings(List<BlazeImportSettings> settings) {
-      importSettings = settings;
-    }
-  }
-}
diff --git a/base/src/com/google/idea/blaze/base/sync/BlazeSyncTask.java b/base/src/com/google/idea/blaze/base/sync/BlazeSyncTask.java
index 9b6849f..5504048 100644
--- a/base/src/com/google/idea/blaze/base/sync/BlazeSyncTask.java
+++ b/base/src/com/google/idea/blaze/base/sync/BlazeSyncTask.java
@@ -16,7 +16,6 @@
 package com.google.idea.blaze.base.sync;
 
 import com.google.common.annotations.VisibleForTesting;
-import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableMap;
 import com.google.common.collect.ImmutableMultimap;
 import com.google.common.collect.Iterables;
@@ -26,6 +25,7 @@
 import com.google.common.util.concurrent.ListeningExecutorService;
 import com.google.idea.blaze.base.async.FutureUtil;
 import com.google.idea.blaze.base.async.executor.BlazeExecutor;
+import com.google.idea.blaze.base.command.BlazeFlags;
 import com.google.idea.blaze.base.command.info.BlazeInfo;
 import com.google.idea.blaze.base.experiments.ExperimentScope;
 import com.google.idea.blaze.base.filecache.FileCaches;
@@ -72,6 +72,7 @@
 import com.google.idea.blaze.base.sync.libraries.BlazeLibraryCollector;
 import com.google.idea.blaze.base.sync.libraries.LibraryEditor;
 import com.google.idea.blaze.base.sync.projectstructure.ContentEntryEditor;
+import com.google.idea.blaze.base.sync.projectstructure.DirectoryStructure;
 import com.google.idea.blaze.base.sync.projectstructure.ModuleEditorImpl;
 import com.google.idea.blaze.base.sync.projectstructure.ModuleEditorProvider;
 import com.google.idea.blaze.base.sync.projectview.ImportRoots;
@@ -208,12 +209,23 @@
       return SyncResult.FAILURE;
     }
 
+    ListeningExecutorService executor = BlazeExecutor.getInstance().getExecutor();
+    WorkspacePathResolverAndProjectView workspacePathResolverAndProjectView =
+        computeWorkspacePathResolverAndProjectView(context, vcsHandler, executor);
+    if (workspacePathResolverAndProjectView == null) {
+      return SyncResult.FAILURE;
+    }
+    ProjectViewSet projectViewSet = workspacePathResolverAndProjectView.projectViewSet;
+
     ListenableFuture<ImmutableMap<String, String>> blazeInfoFuture =
         BlazeInfo.getInstance()
             .runBlazeInfo(
-                context, importSettings.getBuildSystem(), workspaceRoot, ImmutableList.of());
+                context,
+                Blaze.getBuildSystemProvider(project).getSyncBinaryPath(),
+                workspaceRoot,
+                BlazeFlags.buildFlags(project, projectViewSet));
 
-    ListeningExecutorService executor = BlazeExecutor.getInstance().getExecutor();
+
     ListenableFuture<WorkingSet> workingSetFuture =
         vcsHandler.getWorkingSet(project, context, workspaceRoot, executor);
 
@@ -228,21 +240,14 @@
     if (blazeInfo == null) {
       return SyncResult.FAILURE;
     }
-    BlazeRoots blazeRoots =
-        BlazeRoots.build(importSettings.getBuildSystem(), workspaceRoot, blazeInfo);
+    BlazeRoots blazeRoots = BlazeRoots.build(importSettings.getBuildSystem(), blazeInfo);
     BlazeVersionData blazeVersionData =
         BlazeVersionData.build(importSettings.getBuildSystem(), workspaceRoot, blazeInfo);
 
-    WorkspacePathResolverAndProjectView workspacePathResolverAndProjectView =
-        computeWorkspacePathResolverAndProjectView(context, blazeRoots, vcsHandler, executor);
-    if (workspacePathResolverAndProjectView == null) {
-      return SyncResult.FAILURE;
-    }
     WorkspacePathResolver workspacePathResolver =
         workspacePathResolverAndProjectView.workspacePathResolver;
     ArtifactLocationDecoder artifactLocationDecoder =
         new ArtifactLocationDecoderImpl(blazeRoots, workspacePathResolver);
-    ProjectViewSet projectViewSet = workspacePathResolverAndProjectView.projectViewSet;
 
     WorkspaceLanguageSettings workspaceLanguageSettings =
         LanguageSupport.createWorkspaceLanguageSettings(context, projectViewSet);
@@ -396,11 +401,30 @@
         .onError("Prefetch failed")
         .run();
 
+    ListenableFuture<DirectoryStructure> directoryStructureFuture =
+        DirectoryStructure.getRootDirectoryStructure(project, workspaceRoot, projectViewSet);
+
     refreshVirtualFileSystem(context, newBlazeProjectData);
 
+    DirectoryStructure directoryStructure =
+        FutureUtil.waitForFuture(context, directoryStructureFuture)
+            .withProgressMessage("Computing directory structure...")
+            .timed("DirectoryStructure")
+            .onError("Directory structure computation failed")
+            .run()
+            .result();
+    if (directoryStructure == null) {
+      return SyncResult.FAILURE;
+    }
+
     boolean success =
         updateProject(
-            context, projectViewSet, blazeVersionData, oldBlazeProjectData, newBlazeProjectData);
+            context,
+            projectViewSet,
+            blazeVersionData,
+            directoryStructure,
+            oldBlazeProjectData,
+            newBlazeProjectData);
     if (!success) {
       return SyncResult.FAILURE;
     }
@@ -458,7 +482,6 @@
 
   private WorkspacePathResolverAndProjectView computeWorkspacePathResolverAndProjectView(
       BlazeContext context,
-      BlazeRoots blazeRoots,
       BlazeVcsHandler vcsHandler,
       ListeningExecutorService executor) {
     context.output(new StatusOutput("Updating VCS..."));
@@ -484,7 +507,7 @@
       WorkspacePathResolver workspacePathResolver =
           vcsWorkspacePathResolver != null
               ? vcsWorkspacePathResolver
-              : new WorkspacePathResolverImpl(workspaceRoot, blazeRoots);
+              : new WorkspacePathResolverImpl(workspaceRoot);
 
       ProjectViewSet projectViewSet =
           ProjectViewManager.getInstance(project).reloadProjectView(context, workspacePathResolver);
@@ -639,6 +662,7 @@
       BlazeContext parentContext,
       ProjectViewSet projectViewSet,
       BlazeVersionData blazeVersionData,
+      DirectoryStructure directoryStructure,
       @Nullable BlazeProjectData oldBlazeProjectData,
       BlazeProjectData newBlazeProjectData) {
     return Scope.push(
@@ -656,19 +680,15 @@
                                 () ->
                                     ProjectRootManagerEx.getInstanceEx(this.project)
                                         .mergeRootsChangesDuring(
-                                            () -> {
-                                              updateProjectSdk(
-                                                  context,
-                                                  projectViewSet,
-                                                  blazeVersionData,
-                                                  newBlazeProjectData);
-                                              updateProjectStructure(
-                                                  context,
-                                                  importSettings,
-                                                  projectViewSet,
-                                                  newBlazeProjectData,
-                                                  oldBlazeProjectData);
-                                            })));
+                                            () ->
+                                                updateProjectStructure(
+                                                    context,
+                                                    importSettings,
+                                                    projectViewSet,
+                                                    blazeVersionData,
+                                                    directoryStructure,
+                                                    newBlazeProjectData,
+                                                    oldBlazeProjectData))));
           } catch (Throwable e) {
             IssueOutput.error("Internal error. Error: " + e).submit(context);
             logger.error(e);
@@ -681,24 +701,20 @@
         });
   }
 
-  private void updateProjectSdk(
-      BlazeContext context,
-      ProjectViewSet projectViewSet,
-      BlazeVersionData blazeVersionData,
-      BlazeProjectData newBlazeProjectData) {
-    for (BlazeSyncPlugin syncPlugin : BlazeSyncPlugin.EP_NAME.getExtensions()) {
-      syncPlugin.updateProjectSdk(
-          project, context, projectViewSet, blazeVersionData, newBlazeProjectData);
-    }
-  }
-
   private void updateProjectStructure(
       BlazeContext context,
       BlazeImportSettings importSettings,
       ProjectViewSet projectViewSet,
+      BlazeVersionData blazeVersionData,
+      DirectoryStructure directoryStructure,
       BlazeProjectData newBlazeProjectData,
       @Nullable BlazeProjectData oldBlazeProjectData) {
 
+    for (BlazeSyncPlugin syncPlugin : BlazeSyncPlugin.EP_NAME.getExtensions()) {
+      syncPlugin.updateProjectSdk(
+          project, context, projectViewSet, blazeVersionData, newBlazeProjectData);
+    }
+
     ModuleEditorImpl moduleEditor =
         ModuleEditorProvider.getInstance().getModuleEditor(project, importSettings);
 
@@ -721,7 +737,12 @@
     ModifiableRootModel workspaceModifiableModel = moduleEditor.editModule(workspaceModule);
 
     ContentEntryEditor.createContentEntries(
-        project, workspaceRoot, projectViewSet, newBlazeProjectData, workspaceModifiableModel);
+        project,
+        workspaceRoot,
+        projectViewSet,
+        newBlazeProjectData,
+        directoryStructure,
+        workspaceModifiableModel);
 
     List<BlazeLibrary> libraries = BlazeLibraryCollector.getLibraries(newBlazeProjectData);
     LibraryEditor.updateProjectLibraries(project, context, newBlazeProjectData, libraries);
diff --git a/base/src/com/google/idea/blaze/base/sync/aspects/BlazeIdeInterfaceAspectsImpl.java b/base/src/com/google/idea/blaze/base/sync/aspects/BlazeIdeInterfaceAspectsImpl.java
index 12a09c5..ac83b63 100644
--- a/base/src/com/google/idea/blaze/base/sync/aspects/BlazeIdeInterfaceAspectsImpl.java
+++ b/base/src/com/google/idea/blaze/base/sync/aspects/BlazeIdeInterfaceAspectsImpl.java
@@ -26,6 +26,7 @@
 import com.google.idea.blaze.base.async.executor.BlazeExecutor;
 import com.google.idea.blaze.base.async.process.ExternalTask;
 import com.google.idea.blaze.base.async.process.LineProcessingOutputStream;
+import com.google.idea.blaze.base.bazel.BuildSystemProvider;
 import com.google.idea.blaze.base.command.BlazeCommand;
 import com.google.idea.blaze.base.command.BlazeCommandName;
 import com.google.idea.blaze.base.command.BlazeFlags;
@@ -49,7 +50,6 @@
 import com.google.idea.blaze.base.scope.output.PrintOutput;
 import com.google.idea.blaze.base.scope.scopes.TimingScope;
 import com.google.idea.blaze.base.settings.Blaze;
-import com.google.idea.blaze.base.settings.Blaze.BuildSystem;
 import com.google.idea.blaze.base.sync.aspects.strategy.AspectStrategy;
 import com.google.idea.blaze.base.sync.aspects.strategy.AspectStrategyProvider;
 import com.google.idea.blaze.base.sync.projectview.WorkspaceLanguageSettings;
@@ -192,9 +192,8 @@
 
           List<File> result = Lists.newArrayList();
 
-          BuildSystem buildSystem = Blaze.getBuildSystem(project);
           BlazeCommand.Builder blazeCommandBuilder =
-              BlazeCommand.builder(buildSystem, BlazeCommandName.BUILD);
+              BlazeCommand.builder(getBinaryPath(project), BlazeCommandName.BUILD);
           blazeCommandBuilder.addTargets(targets);
           blazeCommandBuilder.addBlazeFlags(BlazeFlags.KEEP_GOING);
           blazeCommandBuilder
@@ -409,7 +408,7 @@
     AspectStrategy aspectStrategy = getAspectStrategy(project, blazeVersionData);
 
     BlazeCommand.Builder blazeCommandBuilder =
-        BlazeCommand.builder(Blaze.getBuildSystem(project), BlazeCommandName.BUILD)
+        BlazeCommand.builder(getBinaryPath(project), BlazeCommandName.BUILD)
             .addTargets(targets)
             .addBlazeFlags()
             .addBlazeFlags(BlazeFlags.KEEP_GOING)
@@ -447,4 +446,9 @@
     // Should never get here
     throw new IllegalStateException("No aspect strategy found.");
   }
+
+  private static String getBinaryPath(Project project) {
+    BuildSystemProvider buildSystemProvider = Blaze.getBuildSystemProvider(project);
+    return buildSystemProvider.getSyncBinaryPath();
+  }
 }
diff --git a/base/src/com/google/idea/blaze/base/sync/aspects/strategy/AspectStrategyProviderBazel.java b/base/src/com/google/idea/blaze/base/sync/aspects/strategy/AspectStrategyProviderBazel.java
index 180ae9e..c8a4920 100644
--- a/base/src/com/google/idea/blaze/base/sync/aspects/strategy/AspectStrategyProviderBazel.java
+++ b/base/src/com/google/idea/blaze/base/sync/aspects/strategy/AspectStrategyProviderBazel.java
@@ -21,12 +21,12 @@
 
 class AspectStrategyProviderBazel implements AspectStrategyProvider {
   private static final BoolExperiment useSkylarkAspect =
-      new BoolExperiment("use.skylark.aspect.bazel.2", true);
+      new BoolExperiment("use.skylark.aspect.bazel.3", true);
 
   @Override
   public AspectStrategy getAspectStrategy(Project project, BlazeVersionData blazeVersionData) {
     boolean canUseSkylark =
-        useSkylarkAspect.getValue() && blazeVersionData.bazelIsAtLeastVersion(0, 4, 4);
+        useSkylarkAspect.getValue() && blazeVersionData.bazelIsAtLeastVersion(0, 4, 5);
 
     return canUseSkylark ? new AspectStrategySkylark() : new AspectStrategyNative();
   }
diff --git a/base/src/com/google/idea/blaze/base/sync/projectstructure/ContentEntryEditor.java b/base/src/com/google/idea/blaze/base/sync/projectstructure/ContentEntryEditor.java
index 5b76a0c..b179a7d 100644
--- a/base/src/com/google/idea/blaze/base/sync/projectstructure/ContentEntryEditor.java
+++ b/base/src/com/google/idea/blaze/base/sync/projectstructure/ContentEntryEditor.java
@@ -19,7 +19,6 @@
 import com.google.common.collect.ImmutableMap;
 import com.google.common.collect.Lists;
 import com.google.common.collect.Multimap;
-import com.google.idea.blaze.base.io.FileAttributeProvider;
 import com.google.idea.blaze.base.model.BlazeProjectData;
 import com.google.idea.blaze.base.model.primitives.WorkspacePath;
 import com.google.idea.blaze.base.model.primitives.WorkspaceRoot;
@@ -36,6 +35,7 @@
 import java.io.File;
 import java.util.Collection;
 import java.util.List;
+import java.util.Map;
 
 /** Modifies content entries based on project data. */
 public class ContentEntryEditor {
@@ -45,6 +45,7 @@
       WorkspaceRoot workspaceRoot,
       ProjectViewSet projectViewSet,
       BlazeProjectData blazeProjectData,
+      DirectoryStructure rootDirectoryStructure,
       ModifiableRootModel modifiableRootModel) {
     ImportRoots importRoots =
         ImportRoots.builder(workspaceRoot, Blaze.getBuildSystem(project))
@@ -60,9 +61,9 @@
 
     List<ContentEntry> contentEntries = Lists.newArrayList();
     for (WorkspacePath rootDirectory : rootDirectories) {
-      File root = workspaceRoot.fileForPath(rootDirectory);
+      File rootFile = workspaceRoot.fileForPath(rootDirectory);
       ContentEntry contentEntry =
-          modifiableRootModel.addContentEntry(UrlUtil.pathToUrl(root.getPath()));
+          modifiableRootModel.addContentEntry(UrlUtil.pathToUrl(rootFile.getPath()));
       contentEntries.add(contentEntry);
 
       for (WorkspacePath exclude : excludesByRootDirectory.get(rootDirectory)) {
@@ -72,7 +73,7 @@
 
       ImmutableMap<File, SourceFolder> sourceFolders =
           provider.initializeSourceFolders(contentEntry);
-      SourceFolder rootSource = sourceFolders.get(root);
+      SourceFolder rootSource = sourceFolders.get(rootFile);
       walkFileSystem(
           workspaceRoot,
           testConfig,
@@ -81,7 +82,8 @@
           provider,
           sourceFolders,
           rootSource,
-          root);
+          rootDirectory,
+          rootDirectoryStructure.directories.get(rootDirectory));
     }
   }
 
@@ -93,20 +95,12 @@
       SourceFolderProvider provider,
       ImmutableMap<File, SourceFolder> sourceFolders,
       SourceFolder parent,
-      File file) {
-    if (!FileAttributeProvider.getInstance().isDirectory(file)) {
-      return;
-    }
-    WorkspacePath workspacePath;
-    try {
-      workspacePath = workspaceRoot.workspacePathFor(file);
-    } catch (IllegalArgumentException e) {
-      // stop at directories with unhandled characters.
-      return;
-    }
+      WorkspacePath workspacePath,
+      DirectoryStructure directoryStructure) {
     if (excludedDirectories.contains(workspacePath)) {
       return;
     }
+    File file = workspaceRoot.fileForPath(workspacePath);
     boolean isTest = testConfig.isTestSource(workspacePath.relativePath());
     SourceFolder current = sourceFolders.get(new File(file.getPath()));
     SourceFolder currentOrParent = current != null ? current : parent;
@@ -117,11 +111,8 @@
         contentEntry.removeSourceFolder(current);
       }
     }
-    File[] children = FileAttributeProvider.getInstance().listFiles(file);
-    if (children == null) {
-      return;
-    }
-    for (File child : children) {
+    for (Map.Entry<WorkspacePath, DirectoryStructure> child :
+        directoryStructure.directories.entrySet()) {
       walkFileSystem(
           workspaceRoot,
           testConfig,
@@ -130,7 +121,8 @@
           provider,
           sourceFolders,
           currentOrParent,
-          child);
+          child.getKey(),
+          child.getValue());
     }
   }
 
diff --git a/base/src/com/google/idea/blaze/base/sync/projectstructure/DirectoryStructure.java b/base/src/com/google/idea/blaze/base/sync/projectstructure/DirectoryStructure.java
new file mode 100644
index 0000000..827e13e
--- /dev/null
+++ b/base/src/com/google/idea/blaze/base/sync/projectstructure/DirectoryStructure.java
@@ -0,0 +1,89 @@
+/*
+ * Copyright 2016 The Bazel Authors. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.idea.blaze.base.sync.projectstructure;
+
+import com.google.common.collect.ImmutableMap;
+import com.google.common.util.concurrent.ListenableFuture;
+import com.google.idea.blaze.base.io.FileAttributeProvider;
+import com.google.idea.blaze.base.model.primitives.WorkspacePath;
+import com.google.idea.blaze.base.model.primitives.WorkspaceRoot;
+import com.google.idea.blaze.base.prefetch.FetchExecutor;
+import com.google.idea.blaze.base.projectview.ProjectViewSet;
+import com.google.idea.blaze.base.settings.Blaze;
+import com.google.idea.blaze.base.sync.projectview.ImportRoots;
+import com.intellij.openapi.project.Project;
+import java.io.File;
+import java.util.Collection;
+
+/**
+ * Directory structure representation used by {@link ContentEntryEditor}.
+ *
+ * <p>The purpose of this class is to pull out all file system operations out of the project
+ * structure commit step, as this step locks the UI.
+ */
+public class DirectoryStructure {
+
+  final ImmutableMap<WorkspacePath, DirectoryStructure> directories;
+
+  private DirectoryStructure(ImmutableMap<WorkspacePath, DirectoryStructure> directories) {
+    this.directories = directories;
+  }
+
+  public static ListenableFuture<DirectoryStructure> getRootDirectoryStructure(
+      Project project, WorkspaceRoot workspaceRoot, ProjectViewSet projectViewSet) {
+    return FetchExecutor.EXECUTOR.submit(
+        () -> computeRootDirectoryStructure(project, workspaceRoot, projectViewSet));
+  }
+
+  private static DirectoryStructure computeRootDirectoryStructure(
+      Project project, WorkspaceRoot workspaceRoot, ProjectViewSet projectViewSet) {
+    ImportRoots importRoots =
+        ImportRoots.builder(workspaceRoot, Blaze.getBuildSystem(project))
+            .add(projectViewSet)
+            .build();
+    Collection<WorkspacePath> rootDirectories = importRoots.rootDirectories();
+    ImmutableMap.Builder<WorkspacePath, DirectoryStructure> result = ImmutableMap.builder();
+    for (WorkspacePath rootDirectory : rootDirectories) {
+      walkDirectoryStructure(workspaceRoot, result, rootDirectory);
+    }
+    return new DirectoryStructure(result.build());
+  }
+
+  private static void walkDirectoryStructure(
+      WorkspaceRoot workspaceRoot,
+      ImmutableMap.Builder<WorkspacePath, DirectoryStructure> parent,
+      WorkspacePath workspacePath) {
+    File file = workspaceRoot.fileForPath(workspacePath);
+    if (!FileAttributeProvider.getInstance().isDirectory(file)) {
+      return;
+    }
+    ImmutableMap.Builder<WorkspacePath, DirectoryStructure> result = ImmutableMap.builder();
+    File[] children = FileAttributeProvider.getInstance().listFiles(file);
+    if (children != null) {
+      for (File child : children) {
+        WorkspacePath childWorkspacePath;
+        try {
+          childWorkspacePath = workspaceRoot.workspacePathFor(child);
+        } catch (IllegalArgumentException e) {
+          // stop at directories with unhandled characters.
+          continue;
+        }
+        walkDirectoryStructure(workspaceRoot, result, childWorkspacePath);
+      }
+    }
+    parent.put(workspacePath, new DirectoryStructure(result.build()));
+  }
+}
diff --git a/base/src/com/google/idea/blaze/base/sync/status/BlazeSyncStatusImpl.java b/base/src/com/google/idea/blaze/base/sync/status/BlazeSyncStatusImpl.java
index ee60cd7..5f74602 100644
--- a/base/src/com/google/idea/blaze/base/sync/status/BlazeSyncStatusImpl.java
+++ b/base/src/com/google/idea/blaze/base/sync/status/BlazeSyncStatusImpl.java
@@ -37,8 +37,8 @@
 import com.intellij.openapi.vfs.newvfs.NewVirtualFile;
 import java.util.Collection;
 import java.util.concurrent.atomic.AtomicBoolean;
+import javax.annotation.Nullable;
 import org.jetbrains.annotations.NotNull;
-import org.jetbrains.annotations.Nullable;
 
 /**
  * Per-project listener for changes to BUILD files, and other changes requiring an incremental sync.
diff --git a/base/src/com/google/idea/blaze/base/sync/workspace/BlazeRoots.java b/base/src/com/google/idea/blaze/base/sync/workspace/BlazeRoots.java
index c66d024..26a00a2 100644
--- a/base/src/com/google/idea/blaze/base/sync/workspace/BlazeRoots.java
+++ b/base/src/com/google/idea/blaze/base/sync/workspace/BlazeRoots.java
@@ -17,43 +17,31 @@
 
 import com.google.common.annotations.VisibleForTesting;
 import com.google.common.collect.ImmutableMap;
-import com.google.common.collect.Lists;
 import com.google.idea.blaze.base.command.info.BlazeInfo;
-import com.google.idea.blaze.base.io.FileAttributeProvider;
 import com.google.idea.blaze.base.model.primitives.ExecutionRootPath;
-import com.google.idea.blaze.base.model.primitives.WorkspaceRoot;
 import com.google.idea.blaze.base.settings.Blaze.BuildSystem;
 import com.intellij.openapi.diagnostic.Logger;
 import java.io.File;
 import java.io.Serializable;
-import java.util.List;
 
 /** The data output by BlazeInfo. */
 public class BlazeRoots implements Serializable {
   public static final long serialVersionUID = 3L;
   private static final Logger logger = Logger.getInstance(BlazeRoots.class);
 
-  public static BlazeRoots build(
-      BuildSystem buildSystem,
-      WorkspaceRoot workspaceRoot,
-      ImmutableMap<String, String> blazeInfo) {
+  public static BlazeRoots build(BuildSystem buildSystem, ImmutableMap<String, String> blazeInfo) {
     return build(
-        workspaceRoot,
         getOrThrow(buildSystem, blazeInfo, BlazeInfo.EXECUTION_ROOT_KEY),
-        getOrThrow(buildSystem, blazeInfo, BlazeInfo.PACKAGE_PATH_KEY),
         getOrThrow(buildSystem, blazeInfo, BlazeInfo.blazeBinKey(buildSystem)),
         getOrThrow(buildSystem, blazeInfo, BlazeInfo.blazeGenfilesKey(buildSystem)),
         getOrThrow(buildSystem, blazeInfo, BlazeInfo.OUTPUT_BASE_KEY));
   }
 
   private static BlazeRoots build(
-      WorkspaceRoot workspaceRoot,
       String execRootString,
-      String packagePathString,
       String blazeBinRoot,
       String blazeGenfilesRoot,
       String externalSourceRoot) {
-    List<File> packagePaths = parsePackagePaths(workspaceRoot.toString(), packagePathString.trim());
     File executionRoot = new File(execRootString.trim());
     ExecutionRootPath blazeBinExecutionRootPath =
         ExecutionRootPath.createAncestorRelativePath(executionRoot, new File(blazeBinRoot));
@@ -64,7 +52,6 @@
     logger.assertTrue(blazeGenfilesExecutionRootPath != null);
     return new BlazeRoots(
         executionRoot,
-        packagePaths,
         blazeBinExecutionRootPath,
         blazeGenfilesExecutionRootPath,
         externalSourceRootFile);
@@ -80,21 +67,7 @@
     return value;
   }
 
-  private static List<File> parsePackagePaths(String workspaceRoot, String packagePathString) {
-    String[] paths = packagePathString.split(":");
-    List<File> packagePaths = Lists.newArrayListWithCapacity(paths.length);
-    FileAttributeProvider fileAttributeProvider = FileAttributeProvider.getInstance();
-    for (String path : paths) {
-      File packagePath = new File(path.replace("%workspace%", workspaceRoot));
-      if (fileAttributeProvider.exists(packagePath)) {
-        packagePaths.add(packagePath);
-      }
-    }
-    return packagePaths;
-  }
-
   public final File executionRoot;
-  public final List<File> packagePaths;
   public final ExecutionRootPath blazeBinExecutionRootPath;
   public final ExecutionRootPath blazeGenfilesExecutionRootPath;
   public final File externalSourceRoot;
@@ -102,12 +75,10 @@
   @VisibleForTesting
   public BlazeRoots(
       File executionRoot,
-      List<File> packagePaths,
       ExecutionRootPath blazeBinExecutionRootPath,
       ExecutionRootPath blazeGenfilesExecutionRootPath,
       File externalSourceRoot) {
     this.executionRoot = executionRoot;
-    this.packagePaths = packagePaths;
     this.blazeBinExecutionRootPath = blazeBinExecutionRootPath;
     this.blazeGenfilesExecutionRootPath = blazeGenfilesExecutionRootPath;
     this.externalSourceRoot = externalSourceRoot;
diff --git a/base/src/com/google/idea/blaze/base/sync/workspace/ExecutionRootPathResolver.java b/base/src/com/google/idea/blaze/base/sync/workspace/ExecutionRootPathResolver.java
index c6b7eef..c1b3f29 100644
--- a/base/src/com/google/idea/blaze/base/sync/workspace/ExecutionRootPathResolver.java
+++ b/base/src/com/google/idea/blaze/base/sync/workspace/ExecutionRootPathResolver.java
@@ -16,11 +16,12 @@
 package com.google.idea.blaze.base.sync.workspace;
 
 import com.google.common.collect.ImmutableList;
+import com.google.idea.blaze.base.bazel.BuildSystemProvider;
 import com.google.idea.blaze.base.model.primitives.ExecutionRootPath;
 import com.google.idea.blaze.base.model.primitives.WorkspacePath;
-import com.intellij.openapi.util.io.FileUtil;
+import com.google.idea.blaze.base.model.primitives.WorkspaceRoot;
+import com.google.idea.blaze.base.settings.Blaze.BuildSystem;
 import java.io.File;
-import java.util.List;
 
 /**
  * Converts execution-root-relative paths to absolute files with a minimum of file system calls
@@ -31,15 +32,29 @@
  */
 public class ExecutionRootPathResolver {
 
-  private final BlazeRoots blazeRoots;
+  private final ImmutableList<String> buildArtifactDirectories;
+  private final File executionRoot;
   private final WorkspacePathResolver workspacePathResolver;
 
   public ExecutionRootPathResolver(
-      BlazeRoots blazeRoots, WorkspacePathResolver workspacePathResolver) {
-    this.blazeRoots = blazeRoots;
+      BuildSystem buildSystem,
+      WorkspaceRoot workspaceRoot,
+      File executionRoot,
+      WorkspacePathResolver workspacePathResolver) {
+    this.buildArtifactDirectories = buildArtifactDirectories(buildSystem, workspaceRoot);
+    this.executionRoot = executionRoot;
     this.workspacePathResolver = workspacePathResolver;
   }
 
+  private static ImmutableList<String> buildArtifactDirectories(
+      BuildSystem buildSystem, WorkspaceRoot workspaceRoot) {
+    BuildSystemProvider provider = BuildSystemProvider.getBuildSystemProvider(buildSystem);
+    if (provider == null) {
+      provider = BuildSystemProvider.defaultBuildSystem();
+    }
+    return provider.buildArtifactDirectories(workspaceRoot);
+  }
+
   /**
    * This method should be used for directories. Returns all workspace files corresponding to the
    * given execution-root-relative path. If the file does not exist inside the workspace (e.g. for
@@ -53,19 +68,21 @@
       WorkspacePath workspacePath = new WorkspacePath(path.getAbsoluteOrRelativeFile().getPath());
       return workspacePathResolver.resolveToIncludeDirectories(workspacePath);
     }
-    return ImmutableList.of(path.getFileRootedAt(blazeRoots.executionRoot));
+    return ImmutableList.of(path.getFileRootedAt(executionRoot));
   }
 
   private boolean isInWorkspace(ExecutionRootPath path) {
-    boolean inOutputDir =
-        ExecutionRootPath.isAncestor(blazeRoots.blazeBinExecutionRootPath, path, false)
-            || ExecutionRootPath.isAncestor(blazeRoots.blazeGenfilesExecutionRootPath, path, false)
-            || isExternalWorkspacePath(path);
-    return !inOutputDir;
+    String firstPathComponent = getFirstPathComponent(path.getAbsoluteOrRelativeFile().getPath());
+    return !buildArtifactDirectories.contains(firstPathComponent)
+        && !isExternalWorkspacePath(firstPathComponent);
   }
 
-  private static boolean isExternalWorkspacePath(ExecutionRootPath path) {
-    List<String> pathComponents = FileUtil.splitPath(path.getAbsoluteOrRelativeFile().getPath());
-    return pathComponents.size() > 1 && "external".equals(pathComponents.get(0));
+  private static String getFirstPathComponent(String path) {
+    int index = path.indexOf(File.separatorChar);
+    return index == -1 ? path : path.substring(0, index);
+  }
+
+  private static boolean isExternalWorkspacePath(String firstPathComponent) {
+    return firstPathComponent.equals("external");
   }
 }
diff --git a/base/src/com/google/idea/blaze/base/sync/workspace/WorkspacePathResolverImpl.java b/base/src/com/google/idea/blaze/base/sync/workspace/WorkspacePathResolverImpl.java
index ac131c1..48b4ddc 100644
--- a/base/src/com/google/idea/blaze/base/sync/workspace/WorkspacePathResolverImpl.java
+++ b/base/src/com/google/idea/blaze/base/sync/workspace/WorkspacePathResolverImpl.java
@@ -16,31 +16,19 @@
 package com.google.idea.blaze.base.sync.workspace;
 
 import com.google.common.collect.ImmutableList;
-import com.google.idea.blaze.base.io.FileAttributeProvider;
 import com.google.idea.blaze.base.model.primitives.WorkspacePath;
 import com.google.idea.blaze.base.model.primitives.WorkspaceRoot;
 import java.io.File;
-import java.util.List;
 import javax.annotation.Nullable;
 
 /** Uses the package path locations to resolve a workspace path. */
 public class WorkspacePathResolverImpl implements WorkspacePathResolver {
-  private static final long serialVersionUID = 2L;
+  private static final long serialVersionUID = 3L;
 
   private final WorkspaceRoot workspaceRoot;
-  private final List<File> packagePaths;
-
-  public WorkspacePathResolverImpl(WorkspaceRoot workspaceRoot, BlazeRoots blazeRoots) {
-    this(workspaceRoot, blazeRoots.packagePaths);
-  }
 
   public WorkspacePathResolverImpl(WorkspaceRoot workspaceRoot) {
-    this(workspaceRoot, ImmutableList.of(workspaceRoot.directory()));
-  }
-
-  public WorkspacePathResolverImpl(WorkspaceRoot workspaceRoot, List<File> packagePaths) {
     this.workspaceRoot = workspaceRoot;
-    this.packagePaths = packagePaths;
   }
 
   @Override
@@ -50,19 +38,7 @@
 
   @Override
   public File findPackageRoot(String relativePath) {
-    if (packagePaths.size() == 1) {
-      return packagePaths.get(0);
-    }
-    // fall back to manually checking each one
-    FileAttributeProvider existenceChecker = FileAttributeProvider.getInstance();
-    for (File pkg : packagePaths) {
-      if (existenceChecker.exists(new File(pkg, relativePath))) {
-        return pkg;
-      }
-    }
-
-    // Return first in package path, even though it might not exist
-    return packagePaths.get(0);
+    return workspaceRoot.directory();
   }
 
   @Nullable
diff --git a/base/src/com/google/idea/blaze/base/treeview/BlazeTreeStructureProvider.java b/base/src/com/google/idea/blaze/base/treeview/BlazeTreeStructureProvider.java
index 764e3b0..712c1fc 100644
--- a/base/src/com/google/idea/blaze/base/treeview/BlazeTreeStructureProvider.java
+++ b/base/src/com/google/idea/blaze/base/treeview/BlazeTreeStructureProvider.java
@@ -36,8 +36,8 @@
 import java.io.File;
 import java.util.Collection;
 import java.util.List;
+import javax.annotation.Nullable;
 import org.jetbrains.annotations.NotNull;
-import org.jetbrains.annotations.Nullable;
 
 /**
  * Modifies the project view:
@@ -147,6 +147,9 @@
 
       @Override
       public boolean isShowExcludedFiles() {
+        if (original instanceof ProjectViewSettings) {
+          return ((ProjectViewSettings) original).isShowExcludedFiles();
+        }
         return true;
       }
     };
diff --git a/base/src/com/google/idea/blaze/base/ui/BlazeValidationError.java b/base/src/com/google/idea/blaze/base/ui/BlazeValidationError.java
index 452ae82..f79fffc 100644
--- a/base/src/com/google/idea/blaze/base/ui/BlazeValidationError.java
+++ b/base/src/com/google/idea/blaze/base/ui/BlazeValidationError.java
@@ -18,9 +18,9 @@
 import com.intellij.openapi.project.Project;
 import com.intellij.openapi.ui.Messages;
 import java.util.Collection;
+import javax.annotation.Nullable;
 import javax.annotation.concurrent.Immutable;
 import org.jetbrains.annotations.NotNull;
-import org.jetbrains.annotations.Nullable;
 
 /** An error occuring during a blaze validation */
 @Immutable
diff --git a/base/src/com/google/idea/blaze/base/ui/BlazeValidationResult.java b/base/src/com/google/idea/blaze/base/ui/BlazeValidationResult.java
index 4200003..b6bc270 100644
--- a/base/src/com/google/idea/blaze/base/ui/BlazeValidationResult.java
+++ b/base/src/com/google/idea/blaze/base/ui/BlazeValidationResult.java
@@ -15,7 +15,7 @@
  */
 package com.google.idea.blaze.base.ui;
 
-import org.jetbrains.annotations.Nullable;
+import javax.annotation.Nullable;
 
 /** Pair of (success, validation error) */
 public class BlazeValidationResult {
diff --git a/base/src/com/google/idea/blaze/base/util/SerializationUtil.java b/base/src/com/google/idea/blaze/base/util/SerializationUtil.java
index f098673..ea689da 100644
--- a/base/src/com/google/idea/blaze/base/util/SerializationUtil.java
+++ b/base/src/com/google/idea/blaze/base/util/SerializationUtil.java
@@ -17,7 +17,6 @@
 
 import com.google.common.io.Closeables;
 import com.intellij.CommonBundle;
-import com.intellij.openapi.diagnostic.Logger;
 import java.io.File;
 import java.io.FileInputStream;
 import java.io.FileOutputStream;
@@ -26,12 +25,11 @@
 import java.io.ObjectOutputStream;
 import java.io.ObjectStreamClass;
 import java.io.Serializable;
+import javax.annotation.Nullable;
 import org.jetbrains.annotations.NotNull;
-import org.jetbrains.annotations.Nullable;
 
 /** Utils for serialization. */
 public class SerializationUtil {
-  private static final Logger logger = Logger.getInstance(SerializationUtil.class.getName());
 
   public static void saveToDisk(@NotNull File file, @NotNull Serializable serializable)
       throws IOException {
diff --git a/base/src/com/google/idea/blaze/base/wizard2/BlazeWizardUserSettingsStorage.java b/base/src/com/google/idea/blaze/base/wizard2/BlazeWizardUserSettingsStorage.java
index 2184d14..0ff0440 100644
--- a/base/src/com/google/idea/blaze/base/wizard2/BlazeWizardUserSettingsStorage.java
+++ b/base/src/com/google/idea/blaze/base/wizard2/BlazeWizardUserSettingsStorage.java
@@ -19,7 +19,7 @@
 import com.intellij.openapi.components.ServiceManager;
 import com.intellij.openapi.components.State;
 import com.intellij.openapi.components.Storage;
-import org.jetbrains.annotations.Nullable;
+import javax.annotation.Nullable;
 
 /** Stores wizard user settings between runs. */
 @State(name = "BlazeWizardUserSettings", storages = @Storage("blaze.wizard.settings.xml"))
diff --git a/base/src/com/google/idea/blaze/base/wizard2/ui/BlazeEditProjectViewControl.java b/base/src/com/google/idea/blaze/base/wizard2/ui/BlazeEditProjectViewControl.java
index e936db3..70001eb 100644
--- a/base/src/com/google/idea/blaze/base/wizard2/ui/BlazeEditProjectViewControl.java
+++ b/base/src/com/google/idea/blaze/base/wizard2/ui/BlazeEditProjectViewControl.java
@@ -68,11 +68,11 @@
 import java.io.IOException;
 import java.util.List;
 import java.util.stream.Collectors;
+import javax.annotation.Nullable;
 import javax.swing.JLabel;
 import javax.swing.JPanel;
 import javax.swing.JTextField;
 import org.jetbrains.annotations.NotNull;
-import org.jetbrains.annotations.Nullable;
 
 /** The UI control to collect project settings when importing a Blaze project. */
 public final class BlazeEditProjectViewControl {
diff --git a/base/tests/integrationtests/com/google/idea/blaze/base/lang/buildfile/actions/BuildFileModifierTest.java b/base/tests/integrationtests/com/google/idea/blaze/base/lang/buildfile/actions/BuildFileModifierTest.java
new file mode 100644
index 0000000..311cfdf
--- /dev/null
+++ b/base/tests/integrationtests/com/google/idea/blaze/base/lang/buildfile/actions/BuildFileModifierTest.java
@@ -0,0 +1,46 @@
+/*
+ * Copyright 2017 The Bazel Authors. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.idea.blaze.base.lang.buildfile.actions;
+
+import com.google.idea.blaze.base.buildmodifier.BuildFileModifier;
+import com.google.idea.blaze.base.lang.buildfile.BuildFileIntegrationTestCase;
+import com.google.idea.blaze.base.lang.buildfile.psi.BuildFile;
+import com.google.idea.blaze.base.model.primitives.Kind;
+import com.google.idea.blaze.base.model.primitives.Label;
+import com.google.idea.blaze.base.model.primitives.WorkspacePath;
+import com.google.idea.blaze.base.scope.BlazeContext;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** Integration tests for {@link BuildFileModifier}. */
+@RunWith(JUnit4.class)
+public class BuildFileModifierTest extends BuildFileIntegrationTestCase {
+
+  @Test
+  public void testAddNewTarget() {
+    BuildFile buildFile =
+        createBuildFile(new WorkspacePath("BUILD"), "java_library(name = 'existing')", "");
+    BuildFileModifier.getInstance()
+        .addRule(getProject(), new BlazeContext(), new Label("//:new_target"), Kind.JAVA_TEST);
+    assertFileContents(
+        buildFile,
+        "java_library(name = 'existing')",
+        "java_test(",
+        "    name = \"new_target\"",
+        ")");
+  }
+}
diff --git a/base/tests/integrationtests/com/google/idea/blaze/base/lang/buildfile/completion/SkylarkExtensionSymbolCompletionTest.java b/base/tests/integrationtests/com/google/idea/blaze/base/lang/buildfile/completion/SkylarkExtensionSymbolCompletionTest.java
index 44f3cb9..7a297ac 100644
--- a/base/tests/integrationtests/com/google/idea/blaze/base/lang/buildfile/completion/SkylarkExtensionSymbolCompletionTest.java
+++ b/base/tests/integrationtests/com/google/idea/blaze/base/lang/buildfile/completion/SkylarkExtensionSymbolCompletionTest.java
@@ -96,4 +96,18 @@
     assertThat(editorTest.completeIfUnique()).isTrue();
     assertFileContents(file, "load(':skylark.bzl', 'function')");
   }
+
+  @Test
+  public void testPrivateSymbolsNotAutocompleted() {
+    workspace.createFile(
+        new WorkspacePath("skylark.bzl"),
+        "_local = 1",
+        "GLOBAL_VAR = 2",
+        "def _local_fn():stmt",
+        "def global_fn():stmt");
+    createAndSetCaret(new WorkspacePath("BUILD"), "load(':skylark.bzl', '<caret>')");
+
+    String[] options = editorTest.getCompletionItemsAsStrings();
+    assertThat(options).asList().containsExactly("'GLOBAL_VAR'", "'global_fn'");
+  }
 }
diff --git a/base/tests/integrationtests/com/google/idea/blaze/base/lang/buildfile/quickfix/DeprecatedLoadQuickFixTest.java b/base/tests/integrationtests/com/google/idea/blaze/base/lang/buildfile/quickfix/DeprecatedLoadQuickFixTest.java
new file mode 100644
index 0000000..549334f
--- /dev/null
+++ b/base/tests/integrationtests/com/google/idea/blaze/base/lang/buildfile/quickfix/DeprecatedLoadQuickFixTest.java
@@ -0,0 +1,105 @@
+/*
+ * Copyright 2017 The Bazel Authors. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.idea.blaze.base.lang.buildfile.quickfix;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.google.idea.blaze.base.lang.buildfile.BuildFileIntegrationTestCase;
+import com.google.idea.blaze.base.lang.buildfile.psi.BuildFile;
+import com.google.idea.blaze.base.lang.buildfile.psi.StringLiteral;
+import com.google.idea.blaze.base.lang.buildfile.psi.util.PsiUtils;
+import com.google.idea.blaze.base.model.primitives.WorkspacePath;
+import com.intellij.codeInspection.ProblemDescriptor;
+import com.intellij.codeInspection.ProblemHighlightType;
+import com.intellij.openapi.command.WriteCommandAction;
+import com.intellij.psi.PsiElement;
+import com.intellij.testFramework.MockProblemDescriptor;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** Integration tests for {@link DeprecatedLoadQuickFix}. */
+@RunWith(JUnit4.class)
+public class DeprecatedLoadQuickFixTest extends BuildFileIntegrationTestCase {
+
+  @Test
+  public void testParentDirectoryHasNoBuildFile() {
+    BuildFile file =
+        createBuildFile(
+            new WorkspacePath("java/com/google/BUILD"),
+            "load('/java/com/google/subdir/build_defs.bzl', 'symbol')");
+
+    StringLiteral string = PsiUtils.findFirstChildOfClassRecursive(file, StringLiteral.class);
+    applyQuickFix(string);
+
+    assertThat(string.getStringContents()).isEqualTo("//java/com/google:subdir/build_defs.bzl");
+  }
+
+  @Test
+  public void testBlazePackageIsParentDirectory() {
+    workspace.createPsiFile(new WorkspacePath("foo/bar/BUILD"));
+    workspace.createPsiFile(new WorkspacePath("foo/bar/build_defs.bzl"));
+
+    BuildFile file =
+        createBuildFile(
+            new WorkspacePath("java/com/google/BUILD"),
+            "load('/foo/bar/build_defs.bzl', 'symbol')");
+
+    StringLiteral string = PsiUtils.findFirstChildOfClassRecursive(file, StringLiteral.class);
+    applyQuickFix(string);
+
+    assertThat(string.getStringContents()).isEqualTo("//foo/bar:build_defs.bzl");
+  }
+
+  @Test
+  public void testNormalLoadStatementUntouched() {
+    workspace.createPsiFile(new WorkspacePath("foo/bar/BUILD"));
+    workspace.createPsiFile(new WorkspacePath("foo/bar/build_defs.bzl"));
+
+    BuildFile file =
+        createBuildFile(
+            new WorkspacePath("java/com/google/BUILD"),
+            "load('//foo/bar:build_defs.bzl', 'symbol')");
+
+    StringLiteral string = PsiUtils.findFirstChildOfClassRecursive(file, StringLiteral.class);
+    String prevString = string.getStringContents();
+    applyQuickFix(string);
+    assertThat(string.getStringContents()).isEqualTo(prevString);
+  }
+
+  @Test
+  public void testRelativeLoadStatementUntouched() {
+    workspace.createPsiFile(new WorkspacePath("foo/bar/build_defs.bzl"));
+    BuildFile file =
+        createBuildFile(new WorkspacePath("foo/bar/BUILD"), "load(':build_defs.bzl', 'symbol')");
+
+    StringLiteral string = PsiUtils.findFirstChildOfClassRecursive(file, StringLiteral.class);
+    String prevString = string.getStringContents();
+    applyQuickFix(string);
+    assertThat(string.getStringContents()).isEqualTo(prevString);
+  }
+
+  private void applyQuickFix(StringLiteral string) {
+    WriteCommandAction.runWriteCommandAction(
+        getProject(),
+        () -> DeprecatedLoadQuickFix.INSTANCE.applyFix(getProject(), descriptorForPsi(string)));
+  }
+
+  private static ProblemDescriptor descriptorForPsi(PsiElement psi) {
+    return new MockProblemDescriptor(
+        psi, "mock", ProblemHighlightType.LIKE_DEPRECATED, DeprecatedLoadQuickFix.INSTANCE);
+  }
+}
diff --git a/base/tests/integrationtests/com/google/idea/blaze/base/lang/buildfile/references/LoadedSkylarkExtensionTest.java b/base/tests/integrationtests/com/google/idea/blaze/base/lang/buildfile/references/LoadedSkylarkExtensionTest.java
index b43f778..5cf33ab 100644
--- a/base/tests/integrationtests/com/google/idea/blaze/base/lang/buildfile/references/LoadedSkylarkExtensionTest.java
+++ b/base/tests/integrationtests/com/google/idea/blaze/base/lang/buildfile/references/LoadedSkylarkExtensionTest.java
@@ -17,14 +17,26 @@
 
 import static com.google.common.truth.Truth.assertThat;
 
+import com.google.common.collect.ImmutableMap;
 import com.google.idea.blaze.base.lang.buildfile.BuildFileIntegrationTestCase;
+import com.google.idea.blaze.base.lang.buildfile.language.semantics.BuildLanguageSpec;
+import com.google.idea.blaze.base.lang.buildfile.language.semantics.BuildLanguageSpecProvider;
+import com.google.idea.blaze.base.lang.buildfile.language.semantics.RuleDefinition;
 import com.google.idea.blaze.base.lang.buildfile.psi.BuildFile;
 import com.google.idea.blaze.base.lang.buildfile.psi.FuncallExpression;
 import com.google.idea.blaze.base.lang.buildfile.psi.FunctionStatement;
 import com.google.idea.blaze.base.lang.buildfile.psi.LoadStatement;
 import com.google.idea.blaze.base.lang.buildfile.psi.LoadedSymbol;
+import com.google.idea.blaze.base.lang.buildfile.psi.ReferenceExpression;
+import com.google.idea.blaze.base.lang.buildfile.psi.TargetExpression;
 import com.google.idea.blaze.base.lang.buildfile.psi.util.PsiUtils;
+import com.google.idea.blaze.base.lang.buildfile.search.FindUsages;
 import com.google.idea.blaze.base.model.primitives.WorkspacePath;
+import com.intellij.openapi.project.Project;
+import com.intellij.psi.PsiReference;
+import java.util.Arrays;
+import java.util.stream.Collectors;
+import javax.annotation.Nullable;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.junit.runners.JUnit4;
@@ -67,7 +79,7 @@
   //  BuildFile buildFile = createBuildFile(
   //    "java/com/google/tools/BUILD",
   //    "load(",
-  //    "\"//java/com/google/build_defs.bzl\",",
+  //    "\"/java/com/google/build_defs.bzl\",",
   //    "\"function\"",
   //    ")");
   //
@@ -119,6 +131,71 @@
   }
 
   @Test
+  public void testLoadedSymbolReference() {
+    BuildFile extFile =
+        createBuildFile(new WorkspacePath("java/com/google/tools/build_defs.bzl"), "CONSTANT = 1");
+
+    BuildFile buildFile =
+        createBuildFile(
+            new WorkspacePath("java/com/google/BUILD"),
+            "load(",
+            "\"//java/com/google/tools:build_defs.bzl\",",
+            "\"CONSTANT\"",
+            ")",
+            "NEW_CONSTANT = CONSTANT");
+
+    TargetExpression target =
+        PsiUtils.findFirstChildOfClassRecursive(extFile, TargetExpression.class);
+    ReferenceExpression ref =
+        PsiUtils.findFirstChildOfClassRecursive(buildFile, ReferenceExpression.class);
+    LoadedSymbol loadElement =
+        PsiUtils.findFirstChildOfClassRecursive(buildFile, LoadedSymbol.class);
+
+    assertThat(target).isNotNull();
+    assertThat(ref.getReferencedElement()).isEqualTo(target);
+    assertThat(loadElement.getImport().getReferencedElement()).isEqualTo(target);
+
+    assertThat(
+            Arrays.stream(FindUsages.findAllReferences(target))
+                .map(PsiReference::getElement)
+                .collect(Collectors.toList()))
+        .containsExactly(ref, loadElement.getImport());
+  }
+
+  @Test
+  public void testOverridenBuiltInSymbolReference() {
+    setBuiltInRuleNames("java_library");
+    BuildFile extFile =
+        createBuildFile(
+            new WorkspacePath("java/com/google/tools/build_defs.bzl"), "java_library = rule()");
+
+    BuildFile buildFile =
+        createBuildFile(
+            new WorkspacePath("java/com/google/BUILD"),
+            "load(",
+            "\"//java/com/google/tools:build_defs.bzl\",",
+            "\"java_library\"",
+            ")",
+            "java_library(name = 'name')");
+
+    TargetExpression target =
+        PsiUtils.findFirstChildOfClassRecursive(extFile, TargetExpression.class);
+    FuncallExpression funcall = buildFile.firstChildOfClass(FuncallExpression.class);
+    LoadedSymbol loadElement =
+        PsiUtils.findFirstChildOfClassRecursive(buildFile, LoadedSymbol.class);
+
+    assertThat(target).isNotNull();
+    assertThat(funcall.getReferencedElement()).isEqualTo(target);
+    assertThat(loadElement.getImport().getReferencedElement()).isEqualTo(target);
+
+    assertThat(
+            Arrays.stream(FindUsages.findAllReferences(target))
+                .map(PsiReference::getElement)
+                .collect(Collectors.toList()))
+        .containsExactly(funcall, loadElement.getImport());
+  }
+
+  @Test
   public void testFuncallReference() {
     BuildFile extFile =
         createBuildFile(
@@ -181,4 +258,30 @@
     assertThat(function).isNotNull();
     assertThat(funcall.getReferencedElement()).isEqualTo(function);
   }
+
+  private void setBuiltInRuleNames(String... ruleNames) {
+    ImmutableMap.Builder<String, RuleDefinition> rules = ImmutableMap.builder();
+    for (String name : ruleNames) {
+      rules.put(name, new RuleDefinition(name, ImmutableMap.of(), null));
+    }
+    MockBuildLanguageSpecProvider specProvider = new MockBuildLanguageSpecProvider();
+    specProvider.setRules(rules.build());
+    registerApplicationService(BuildLanguageSpecProvider.class, specProvider);
+    specProvider.setRules(rules.build());
+  }
+
+  private static class MockBuildLanguageSpecProvider implements BuildLanguageSpecProvider {
+
+    BuildLanguageSpec languageSpec = new BuildLanguageSpec(ImmutableMap.of());
+
+    void setRules(ImmutableMap<String, RuleDefinition> rules) {
+      languageSpec = new BuildLanguageSpec(rules);
+    }
+
+    @Nullable
+    @Override
+    public BuildLanguageSpec getLanguageSpec(Project project) {
+      return languageSpec;
+    }
+  }
 }
diff --git a/base/tests/integrationtests/com/google/idea/blaze/base/lang/buildfile/validation/LoadStatementAnnotatorTest.java b/base/tests/integrationtests/com/google/idea/blaze/base/lang/buildfile/validation/LoadStatementAnnotatorTest.java
new file mode 100644
index 0000000..9af84e7
--- /dev/null
+++ b/base/tests/integrationtests/com/google/idea/blaze/base/lang/buildfile/validation/LoadStatementAnnotatorTest.java
@@ -0,0 +1,119 @@
+/*
+ * Copyright 2017 The Bazel Authors. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.idea.blaze.base.lang.buildfile.validation;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.google.idea.blaze.base.lang.buildfile.BuildFileIntegrationTestCase;
+import com.google.idea.blaze.base.lang.buildfile.psi.BuildElement;
+import com.google.idea.blaze.base.lang.buildfile.psi.BuildFile;
+import com.google.idea.blaze.base.lang.buildfile.psi.util.PsiUtils;
+import com.google.idea.blaze.base.model.primitives.WorkspacePath;
+import com.intellij.codeInsight.daemon.impl.AnnotationHolderImpl;
+import com.intellij.lang.annotation.Annotation;
+import com.intellij.lang.annotation.AnnotationHolder;
+import com.intellij.lang.annotation.AnnotationSession;
+import com.intellij.lang.annotation.HighlightSeverity;
+import com.intellij.psi.PsiFile;
+import java.util.List;
+import java.util.stream.Collectors;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** Tests for {@link LoadStatementAnnotator}. */
+@RunWith(JUnit4.class)
+public class LoadStatementAnnotatorTest extends BuildFileIntegrationTestCase {
+
+  @Test
+  public void testNoWarningsInNormalLoad() {
+    BuildFile file =
+        createBuildFile(
+            new WorkspacePath("java/com/google/BUILD"),
+            "load('//tools/ide/build_test.bzl', 'build_test')",
+            "load(':local_file.bzl', 'symbol')");
+    assertNoAnnotations(file);
+  }
+
+  @Test
+  public void testNoWarningsWhenTyping() {
+    BuildFile file = createBuildFile(new WorkspacePath("java/com/google/BUILD"), "load('/')");
+    assertNoAnnotations(file);
+  }
+
+  @Test
+  public void testWarningForDeprecatedFormat() {
+    BuildFile file =
+        createBuildFile(
+            new WorkspacePath("java/com/google/BUILD"),
+            "load('/tools/ide/build_test.bzl', 'build_test')");
+    assertHasAnnotation(
+        file,
+        "Deprecated load syntax; loaded Skylark module should by in label format.",
+        HighlightSeverity.WARNING);
+  }
+
+  @Test
+  public void testErrorForUnrecognizedFormat() {
+    BuildFile file =
+        createBuildFile(
+            new WorkspacePath("java/com/google/BUILD"), "load('not a skylark label', 'symbol')");
+    assertHasAnnotation(
+        file, "Invalid load syntax: missing Skylark module.", HighlightSeverity.ERROR);
+  }
+
+  @Test
+  public void testErrorForPrivateSymbols() {
+    BuildFile file =
+        createBuildFile(
+            new WorkspacePath("java/com/google/BUILD"), "load(':skylark.bzl', '_local_symbol')");
+    assertHasAnnotation(
+        file, "Symbol '_local_symbol' is private and cannot be imported.", HighlightSeverity.ERROR);
+  }
+
+  private void assertNoAnnotations(BuildFile file) {
+    assertThat(validateFile(file)).isEmpty();
+  }
+
+  private void assertHasAnnotation(BuildFile file, String message, HighlightSeverity type) {
+    assertThat(
+            validateFile(file)
+                .stream()
+                .filter(ann -> ann.getSeverity().equals(type))
+                .map(Annotation::getMessage)
+                .collect(Collectors.toList()))
+        .contains(message);
+  }
+
+  private List<Annotation> validateFile(BuildFile file) {
+    LoadStatementAnnotator annotator = createAnnotator(file);
+    PsiUtils.findAllChildrenOfClassRecursive(file, BuildElement.class)
+        .forEach(element -> element.accept(annotator));
+    return annotationHolder;
+  }
+
+  private LoadStatementAnnotator createAnnotator(PsiFile file) {
+    annotationHolder = new AnnotationHolderImpl(new AnnotationSession(file));
+    return new LoadStatementAnnotator() {
+      @Override
+      protected AnnotationHolder getHolder() {
+        return annotationHolder;
+      }
+    };
+  }
+
+  private AnnotationHolderImpl annotationHolder = null;
+}
diff --git a/base/tests/integrationtests/com/google/idea/blaze/base/run/TestTargetHeuristicTest.java b/base/tests/integrationtests/com/google/idea/blaze/base/run/TestTargetHeuristicTest.java
index 6cd2d7c..c68a696 100644
--- a/base/tests/integrationtests/com/google/idea/blaze/base/run/TestTargetHeuristicTest.java
+++ b/base/tests/integrationtests/com/google/idea/blaze/base/run/TestTargetHeuristicTest.java
@@ -19,12 +19,19 @@
 
 import com.google.common.collect.ImmutableList;
 import com.google.idea.blaze.base.BlazeIntegrationTestCase;
+import com.google.idea.blaze.base.ideinfo.ArtifactLocation;
 import com.google.idea.blaze.base.ideinfo.TargetIdeInfo;
 import com.google.idea.blaze.base.ideinfo.TestIdeInfo;
 import com.google.idea.blaze.base.ideinfo.TestIdeInfo.TestSize;
+import com.google.idea.blaze.base.model.BlazeProjectData;
+import com.google.idea.blaze.base.model.MockBlazeProjectDataBuilder;
+import com.google.idea.blaze.base.model.MockBlazeProjectDataManager;
 import com.google.idea.blaze.base.model.primitives.Label;
+import com.google.idea.blaze.base.model.primitives.WorkspacePath;
+import com.google.idea.blaze.base.sync.data.BlazeProjectDataManager;
 import java.io.File;
 import java.util.Collection;
+import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.junit.runners.JUnit4;
@@ -33,9 +40,16 @@
 @RunWith(JUnit4.class)
 public class TestTargetHeuristicTest extends BlazeIntegrationTestCase {
 
+  @Before
+  public final void doSetup() {
+    BlazeProjectData blazeProjectData = MockBlazeProjectDataBuilder.builder(workspaceRoot).build();
+    registerProjectService(
+        BlazeProjectDataManager.class, new MockBlazeProjectDataManager(blazeProjectData));
+  }
+
   @Test
-  public void testTestSizeMatched() throws Exception {
-    File source = new File("java/com/foo/FooTest.java");
+  public void testTestSizeMatched() {
+    File source = workspaceRoot.fileForPath(new WorkspacePath("java/com/foo/FooTest.java"));
     Collection<TargetIdeInfo> targets =
         ImmutableList.of(
             TargetIdeInfo.builder()
@@ -49,24 +63,46 @@
                 .setTestInfo(TestIdeInfo.builder().setTestSize(TestSize.SMALL))
                 .build());
     Label match =
-        TestTargetHeuristic.chooseTestTargetForSourceFile(source, targets, TestSize.SMALL);
+        TestTargetHeuristic.chooseTestTargetForSourceFile(
+            getProject(), source, targets, TestSize.SMALL);
     assertThat(match).isEqualTo(new Label("//foo:test2"));
   }
 
   @Test
-  public void testTargetNameMatched() throws Exception {
-    File source = new File("java/com/foo/FooTest.java");
+  public void testTargetSourcesMatched() {
+    File source = workspaceRoot.fileForPath(new WorkspacePath("java/com/foo/FooTest.java"));
+    Collection<TargetIdeInfo> targets =
+        ImmutableList.of(
+            TargetIdeInfo.builder()
+                .setLabel("//foo:test1")
+                .setKind("java_test")
+                .addSource(sourceRoot("java/com/bar/OtherTest.java"))
+                .build(),
+            TargetIdeInfo.builder()
+                .setLabel("//foo:test2")
+                .setKind("java_test")
+                .addSource(sourceRoot("java/com/foo/FooTest.java"))
+                .build());
+    Label match =
+        TestTargetHeuristic.chooseTestTargetForSourceFile(getProject(), source, targets, null);
+    assertThat(match).isEqualTo(new Label("//foo:test2"));
+  }
+
+  @Test
+  public void testTargetNameMatched() {
+    File source = workspaceRoot.fileForPath(new WorkspacePath("java/com/foo/FooTest.java"));
     Collection<TargetIdeInfo> targets =
         ImmutableList.of(
             TargetIdeInfo.builder().setLabel("//foo:FirstTest").setKind("java_test").build(),
             TargetIdeInfo.builder().setLabel("//foo:FooTest").setKind("java_test").build());
-    Label match = TestTargetHeuristic.chooseTestTargetForSourceFile(source, targets, null);
+    Label match =
+        TestTargetHeuristic.chooseTestTargetForSourceFile(getProject(), source, targets, null);
     assertThat(match).isEqualTo(new Label("//foo:FooTest"));
   }
 
   @Test
-  public void testNoMatchFallBackToFirstTarget() throws Exception {
-    File source = new File("java/com/foo/FooTest.java");
+  public void testNoMatchFallBackToFirstTarget() {
+    File source = workspaceRoot.fileForPath(new WorkspacePath("java/com/foo/FooTest.java"));
     ImmutableList<TargetIdeInfo> targets =
         ImmutableList.of(
             TargetIdeInfo.builder()
@@ -80,13 +116,14 @@
                 .setTestInfo(TestIdeInfo.builder().setTestSize(TestSize.SMALL))
                 .build());
     Label match =
-        TestTargetHeuristic.chooseTestTargetForSourceFile(source, targets, TestSize.LARGE);
+        TestTargetHeuristic.chooseTestTargetForSourceFile(
+            getProject(), source, targets, TestSize.LARGE);
     assertThat(match).isEqualTo(new Label("//bar:BarTest"));
   }
 
   @Test
-  public void testTargetNameCheckedBeforeTestSize() throws Exception {
-    File source = new File("java/com/foo/FooTest.java");
+  public void testTargetNameCheckedBeforeTestSize() {
+    File source = workspaceRoot.fileForPath(new WorkspacePath("java/com/foo/FooTest.java"));
     ImmutableList<TargetIdeInfo> targets =
         ImmutableList.of(
             TargetIdeInfo.builder()
@@ -100,7 +137,35 @@
                 .setTestInfo(TestIdeInfo.builder().setTestSize(TestSize.MEDIUM))
                 .build());
     Label match =
-        TestTargetHeuristic.chooseTestTargetForSourceFile(source, targets, TestSize.SMALL);
+        TestTargetHeuristic.chooseTestTargetForSourceFile(
+            getProject(), source, targets, TestSize.SMALL);
     assertThat(match).isEqualTo(new Label("//foo:FooTest"));
   }
+
+  @Test
+  public void testTargetSourcesCheckedBeforeTestSize() {
+    File source = workspaceRoot.fileForPath(new WorkspacePath("java/com/foo/FooTest.java"));
+    Collection<TargetIdeInfo> targets =
+        ImmutableList.of(
+            TargetIdeInfo.builder()
+                .setLabel("//foo:test1")
+                .setKind("java_test")
+                .setTestInfo(TestIdeInfo.builder().setTestSize(TestSize.SMALL))
+                .addSource(sourceRoot("java/com/bar/OtherTest.java"))
+                .build(),
+            TargetIdeInfo.builder()
+                .setLabel("//foo:test2")
+                .setKind("java_test")
+                .setTestInfo(TestIdeInfo.builder().setTestSize(TestSize.MEDIUM))
+                .addSource(sourceRoot("java/com/foo/FooTest.java"))
+                .build());
+    Label match =
+        TestTargetHeuristic.chooseTestTargetForSourceFile(
+            getProject(), source, targets, TestSize.SMALL);
+    assertThat(match).isEqualTo(new Label("//foo:test2"));
+  }
+
+  private static ArtifactLocation sourceRoot(String relativePath) {
+    return ArtifactLocation.builder().setRelativePath(relativePath).setIsSource(true).build();
+  }
 }
diff --git a/base/tests/integrationtests/com/google/idea/blaze/base/run/producer/AllInPackageBlazeConfigurationProducerTest.java b/base/tests/integrationtests/com/google/idea/blaze/base/run/producer/AllInPackageBlazeConfigurationProducerTest.java
new file mode 100644
index 0000000..7063c58
--- /dev/null
+++ b/base/tests/integrationtests/com/google/idea/blaze/base/run/producer/AllInPackageBlazeConfigurationProducerTest.java
@@ -0,0 +1,75 @@
+/*
+ * Copyright 2017 The Bazel Authors. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.idea.blaze.base.run.producer;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.google.idea.blaze.base.command.BlazeCommandName;
+import com.google.idea.blaze.base.model.primitives.TargetExpression;
+import com.google.idea.blaze.base.model.primitives.WorkspacePath;
+import com.google.idea.blaze.base.run.BlazeCommandRunConfiguration;
+import com.google.idea.blaze.base.run.producers.AllInPackageBlazeConfigurationProducer;
+import com.intellij.execution.actions.ConfigurationContext;
+import com.intellij.execution.actions.ConfigurationFromContext;
+import com.intellij.psi.PsiDirectory;
+import java.util.List;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** Integration tests for {@link AllInPackageBlazeConfigurationProducer}. */
+@RunWith(JUnit4.class)
+public class AllInPackageBlazeConfigurationProducerTest
+    extends BlazeRunConfigurationProducerTestCase {
+
+  @Test
+  public void testProducedFromPsiDirectory() {
+    PsiDirectory directory =
+        workspace.createPsiDirectory(new WorkspacePath("java/com/google/test"));
+    workspace.createPsiFile(
+        new WorkspacePath("java/com/google/test/BUILD"), "java_test(name='unit_tests'");
+
+    ConfigurationContext context = createContextFromPsi(directory);
+    List<ConfigurationFromContext> configurations = context.getConfigurationsFromContext();
+    assertThat(configurations).hasSize(1);
+
+    ConfigurationFromContext fromContext = configurations.get(0);
+    assertThat(fromContext.isProducedBy(AllInPackageBlazeConfigurationProducer.class)).isTrue();
+    assertThat(fromContext.getConfiguration()).isInstanceOf(BlazeCommandRunConfiguration.class);
+
+    BlazeCommandRunConfiguration config =
+        (BlazeCommandRunConfiguration) fromContext.getConfiguration();
+    assertThat(config.getTarget())
+        .isEqualTo(TargetExpression.fromString("//java/com/google/test/...:all"));
+    assertThat(getCommandType(config)).isEqualTo(BlazeCommandName.TEST);
+  }
+
+  @Test
+  public void testDirectoryWithoutBlazePackageChildIsIgnored() {
+    PsiDirectory directory =
+        workspace.createPsiDirectory(new WorkspacePath("java/com/google/test"));
+
+    ConfigurationContext context = createContextFromPsi(directory);
+
+    AllInPackageBlazeConfigurationProducer producer = new AllInPackageBlazeConfigurationProducer();
+    assertThat(producer.createConfigurationFromContext(context)).isNull();
+
+    workspace.createPsiDirectory(new WorkspacePath("java/com/google/test/child_dir"));
+    workspace.createPsiFile(new WorkspacePath("java/com/google/test/child_dir/BUILD"));
+
+    assertThat(producer.createConfigurationFromContext(context)).isNotNull();
+  }
+}
diff --git a/base/tests/integrationtests/com/google/idea/blaze/base/run/producer/BlazeBuildFileRunConfigurationProducerTest.java b/base/tests/integrationtests/com/google/idea/blaze/base/run/producer/BlazeBuildFileRunConfigurationProducerTest.java
new file mode 100644
index 0000000..dcf3e6b
--- /dev/null
+++ b/base/tests/integrationtests/com/google/idea/blaze/base/run/producer/BlazeBuildFileRunConfigurationProducerTest.java
@@ -0,0 +1,90 @@
+/*
+ * Copyright 2017 The Bazel Authors. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.idea.blaze.base.run.producer;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.google.idea.blaze.base.command.BlazeCommandName;
+import com.google.idea.blaze.base.lang.buildfile.psi.FuncallExpression;
+import com.google.idea.blaze.base.lang.buildfile.psi.StringLiteral;
+import com.google.idea.blaze.base.lang.buildfile.psi.util.PsiUtils;
+import com.google.idea.blaze.base.model.primitives.TargetExpression;
+import com.google.idea.blaze.base.model.primitives.WorkspacePath;
+import com.google.idea.blaze.base.run.BlazeCommandRunConfiguration;
+import com.google.idea.blaze.base.run.producers.BlazeBuildFileRunConfigurationProducer;
+import com.intellij.execution.actions.ConfigurationContext;
+import com.intellij.execution.actions.ConfigurationFromContext;
+import com.intellij.psi.PsiFile;
+import java.util.List;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** Integration tests for {@link BlazeBuildFileRunConfigurationProducer}. */
+@RunWith(JUnit4.class)
+public class BlazeBuildFileRunConfigurationProducerTest
+    extends BlazeRunConfigurationProducerTestCase {
+
+  @Test
+  public void testProducedFromFuncallExpression() {
+    PsiFile buildFile =
+        workspace.createPsiFile(
+            new WorkspacePath("java/com/google/test/BUILD"), "java_test(name='unit_tests'");
+
+    FuncallExpression target =
+        PsiUtils.findFirstChildOfClassRecursive(buildFile, FuncallExpression.class);
+    assertThat(target).isNotNull();
+
+    ConfigurationContext context = createContextFromPsi(target);
+    List<ConfigurationFromContext> configurations = context.getConfigurationsFromContext();
+    assertThat(configurations).hasSize(1);
+
+    ConfigurationFromContext fromContext = configurations.get(0);
+    assertThat(fromContext.isProducedBy(BlazeBuildFileRunConfigurationProducer.class)).isTrue();
+    assertThat(fromContext.getConfiguration()).isInstanceOf(BlazeCommandRunConfiguration.class);
+
+    BlazeCommandRunConfiguration config =
+        (BlazeCommandRunConfiguration) fromContext.getConfiguration();
+    assertThat(config.getTarget())
+        .isEqualTo(TargetExpression.fromString("//java/com/google/test:unit_tests"));
+    assertThat(getCommandType(config)).isEqualTo(BlazeCommandName.TEST);
+  }
+
+  @Test
+  public void testProducedWhenInsideFuncallExpression() {
+    PsiFile buildFile =
+        workspace.createPsiFile(
+            new WorkspacePath("java/com/google/test/BUILD"), "java_test(name='unit_tests'");
+
+    StringLiteral nameString =
+        PsiUtils.findFirstChildOfClassRecursive(buildFile, StringLiteral.class);
+    assertThat(nameString).isNotNull();
+
+    ConfigurationContext context = createContextFromPsi(nameString);
+    List<ConfigurationFromContext> configurations = context.getConfigurationsFromContext();
+    assertThat(configurations).hasSize(1);
+
+    ConfigurationFromContext fromContext = configurations.get(0);
+    assertThat(fromContext.isProducedBy(BlazeBuildFileRunConfigurationProducer.class)).isTrue();
+    assertThat(fromContext.getConfiguration()).isInstanceOf(BlazeCommandRunConfiguration.class);
+
+    BlazeCommandRunConfiguration config =
+        (BlazeCommandRunConfiguration) fromContext.getConfiguration();
+    assertThat(config.getTarget())
+        .isEqualTo(TargetExpression.fromString("//java/com/google/test:unit_tests"));
+    assertThat(getCommandType(config)).isEqualTo(BlazeCommandName.TEST);
+  }
+}
diff --git a/base/tests/unittests/com/google/idea/blaze/base/actions/BlazeBuildServiceTest.java b/base/tests/unittests/com/google/idea/blaze/base/actions/BlazeBuildServiceTest.java
index cce3237..607aa15 100644
--- a/base/tests/unittests/com/google/idea/blaze/base/actions/BlazeBuildServiceTest.java
+++ b/base/tests/unittests/com/google/idea/blaze/base/actions/BlazeBuildServiceTest.java
@@ -59,7 +59,7 @@
 
   @Override
   protected void initTest(Container applicationServices, Container projectServices) {
-    BlazeImportSettingsManager importSettingsManager = new BlazeImportSettingsManager(project);
+    BlazeImportSettingsManager importSettingsManager = new BlazeImportSettingsManager();
     importSettingsManager.setImportSettings(
         new BlazeImportSettings("", "", "", "", "", Blaze.BuildSystem.Blaze));
     projectServices.register(BlazeImportSettingsManager.class, importSettingsManager);
diff --git a/base/tests/unittests/com/google/idea/blaze/base/command/BlazeCommandTest.java b/base/tests/unittests/com/google/idea/blaze/base/command/BlazeCommandTest.java
index 64b6894..d50882b 100644
--- a/base/tests/unittests/com/google/idea/blaze/base/command/BlazeCommandTest.java
+++ b/base/tests/unittests/com/google/idea/blaze/base/command/BlazeCommandTest.java
@@ -21,7 +21,6 @@
 import com.google.idea.blaze.base.BlazeTestCase;
 import com.google.idea.blaze.base.model.primitives.Label;
 import com.google.idea.blaze.base.model.primitives.TargetExpression;
-import com.google.idea.blaze.base.settings.Blaze.BuildSystem;
 import com.google.idea.blaze.base.settings.BlazeUserSettings;
 import com.google.idea.common.experiments.ExperimentService;
 import com.google.idea.common.experiments.MockExperimentService;
@@ -47,7 +46,7 @@
   @Test
   public void addedFlagsShouldGoAtStart() {
     List<String> flagsCommand =
-        BlazeCommand.builder(BuildSystem.Blaze, BlazeCommandName.RUN)
+        BlazeCommand.builder("/usr/bin/blaze", BlazeCommandName.RUN)
             .addTargets(new Label("//a:b"))
             .addBlazeFlags("--flag1", "--flag2")
             .addExeFlags("--exeFlag1", "--exeFlag2")
@@ -60,7 +59,7 @@
   @Test
   public void targetsShouldGoAfterBlazeFlagsAndDoubleHyphen() {
     List<String> command =
-        BlazeCommand.builder(BuildSystem.Blaze, BlazeCommandName.RUN)
+        BlazeCommand.builder("/usr/bin/blaze", BlazeCommandName.RUN)
             .addTargets(new Label("//a:b"), new Label("//c:d"))
             .addBlazeFlags("--flag1", "--flag2")
             .addExeFlags("--exeFlag1", "--exeFlag2")
@@ -75,7 +74,7 @@
   @Test
   public void exeFlagsShouldGoLast() {
     List<String> command =
-        BlazeCommand.builder(BuildSystem.Blaze, BlazeCommandName.RUN)
+        BlazeCommand.builder("/usr/bin/blaze", BlazeCommandName.RUN)
             .addTargets(new Label("//a:b"), new Label("//c:d"))
             .addBlazeFlags("--flag1", "--flag2")
             .addExeFlags("--exeFlag1", "--exeFlag2")
@@ -88,7 +87,7 @@
   @Test
   public void maintainUserOrderingOfTargets() {
     List<String> command =
-        BlazeCommand.builder(BuildSystem.Blaze, BlazeCommandName.RUN)
+        BlazeCommand.builder("/usr/bin/blaze", BlazeCommandName.RUN)
             .addTargets(
                 new Label("//a:b"), TargetExpression.fromString("-//e:f"), new Label("//c:d"))
             .addBlazeFlags("--flag1", "--flag2")
@@ -116,7 +115,7 @@
   @Test
   public void binaryAndCommandShouldComeFirst() {
     List<String> command =
-        BlazeCommand.builder(BuildSystem.Blaze, BlazeCommandName.BUILD)
+        BlazeCommand.builder("/usr/bin/blaze", BlazeCommandName.BUILD)
             .addBlazeFlags("--flag")
             .addExeFlags("--exeFlag")
             .build()
diff --git a/base/tests/unittests/com/google/idea/blaze/base/model/blaze/deepequalstester/DeepEqualsTesterUtil.java b/base/tests/unittests/com/google/idea/blaze/base/model/blaze/deepequalstester/DeepEqualsTesterUtil.java
index 2ef9472..2f2457b 100644
--- a/base/tests/unittests/com/google/idea/blaze/base/model/blaze/deepequalstester/DeepEqualsTesterUtil.java
+++ b/base/tests/unittests/com/google/idea/blaze/base/model/blaze/deepequalstester/DeepEqualsTesterUtil.java
@@ -23,8 +23,8 @@
 import java.io.ObjectOutputStream;
 import java.lang.reflect.Field;
 import java.util.List;
+import javax.annotation.Nullable;
 import org.jetbrains.annotations.NotNull;
-import org.jetbrains.annotations.Nullable;
 
 /** Utilities for deep equals testing. */
 @VisibleForTesting
diff --git a/base/tests/unittests/com/google/idea/blaze/base/projectview/parser/ProjectViewParserTest.java b/base/tests/unittests/com/google/idea/blaze/base/projectview/parser/ProjectViewParserTest.java
index 08fea57..52ed8cf 100644
--- a/base/tests/unittests/com/google/idea/blaze/base/projectview/parser/ProjectViewParserTest.java
+++ b/base/tests/unittests/com/google/idea/blaze/base/projectview/parser/ProjectViewParserTest.java
@@ -48,8 +48,8 @@
 import java.io.IOException;
 import java.util.Collection;
 import java.util.Map;
+import javax.annotation.Nullable;
 import org.jetbrains.annotations.NotNull;
-import org.jetbrains.annotations.Nullable;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.junit.runners.JUnit4;
diff --git a/base/tests/unittests/com/google/idea/blaze/base/run/BlazeCommandRunConfigurationTest.java b/base/tests/unittests/com/google/idea/blaze/base/run/BlazeCommandRunConfigurationTest.java
index 2f1cac6..989cb80 100644
--- a/base/tests/unittests/com/google/idea/blaze/base/run/BlazeCommandRunConfigurationTest.java
+++ b/base/tests/unittests/com/google/idea/blaze/base/run/BlazeCommandRunConfigurationTest.java
@@ -54,8 +54,7 @@
     super.initTest(applicationServices, projectServices);
 
     applicationServices.register(UISettings.class, new UISettings());
-    projectServices.register(
-        BlazeImportSettingsManager.class, new BlazeImportSettingsManager(project));
+    projectServices.register(BlazeImportSettingsManager.class, new BlazeImportSettingsManager());
     BlazeImportSettingsManager.getInstance(getProject()).setImportSettings(DUMMY_IMPORT_SETTINGS);
 
     applicationServices.register(ExperimentService.class, new MockExperimentService());
diff --git a/base/tests/unittests/com/google/idea/blaze/base/run/RuleNameHeuristicTest.java b/base/tests/unittests/com/google/idea/blaze/base/run/RuleNameHeuristicTest.java
index 2923947..f7a0857 100644
--- a/base/tests/unittests/com/google/idea/blaze/base/run/RuleNameHeuristicTest.java
+++ b/base/tests/unittests/com/google/idea/blaze/base/run/RuleNameHeuristicTest.java
@@ -46,7 +46,7 @@
     File source = new File("java/com/foo/FooTest.java");
     TargetIdeInfo target =
         TargetIdeInfo.builder().setLabel("//foo:FooTest").setKind("java_test").build();
-    assertThat(new TargetNameHeuristic().matchesSource(target, source, null)).isTrue();
+    assertThat(new TargetNameHeuristic().matchesSource(project, target, source, null)).isTrue();
   }
 
   @Test
@@ -54,7 +54,7 @@
     File source = new File("java/com/foo/FooTest.java");
     TargetIdeInfo target =
         TargetIdeInfo.builder().setLabel("//foo:foo/FooTest").setKind("java_test").build();
-    assertThat(new TargetNameHeuristic().matchesSource(target, source, null)).isTrue();
+    assertThat(new TargetNameHeuristic().matchesSource(project, target, source, null)).isTrue();
   }
 
   @Test
@@ -62,7 +62,7 @@
     File source = new File("java/com/foo/BarFooTest.java");
     TargetIdeInfo target =
         TargetIdeInfo.builder().setLabel("//foo:FooTest").setKind("java_test").build();
-    assertThat(new TargetNameHeuristic().matchesSource(target, source, null)).isFalse();
+    assertThat(new TargetNameHeuristic().matchesSource(project, target, source, null)).isFalse();
   }
 
   @Test
@@ -70,7 +70,7 @@
     File source = new File("java/com/foo/FooTest.java");
     TargetIdeInfo target =
         TargetIdeInfo.builder().setLabel("//foo:bar/FooTest").setKind("java_test").build();
-    assertThat(new TargetNameHeuristic().matchesSource(target, source, null)).isFalse();
+    assertThat(new TargetNameHeuristic().matchesSource(project, target, source, null)).isFalse();
   }
 
   @Test
@@ -78,7 +78,7 @@
     File source = new File("java/com/foo/FooTest.java");
     TargetIdeInfo target =
         TargetIdeInfo.builder().setLabel("//foo:ForTest").setKind("java_test").build();
-    assertThat(new TargetNameHeuristic().matchesSource(target, source, null)).isFalse();
+    assertThat(new TargetNameHeuristic().matchesSource(project, target, source, null)).isFalse();
   }
 
   @Test
@@ -88,7 +88,7 @@
         ImmutableList.of(
             TargetIdeInfo.builder().setLabel("//foo:FirstTest").setKind("java_test").build(),
             TargetIdeInfo.builder().setLabel("//bar:OtherTest").setKind("java_test").build());
-    Label match = TestTargetHeuristic.chooseTestTargetForSourceFile(source, targets, null);
+    Label match = TestTargetHeuristic.chooseTestTargetForSourceFile(project, source, targets, null);
     assertThat(match).isEqualTo(new Label("//foo:FirstTest"));
   }
 
@@ -99,7 +99,7 @@
         ImmutableList.of(
             TargetIdeInfo.builder().setLabel("//bar:FirstTest").setKind("java_test").build(),
             TargetIdeInfo.builder().setLabel("//foo:FooTest").setKind("java_test").build());
-    Label match = TestTargetHeuristic.chooseTestTargetForSourceFile(source, targets, null);
+    Label match = TestTargetHeuristic.chooseTestTargetForSourceFile(project, source, targets, null);
     assertThat(match).isEqualTo(new Label("//foo:FooTest"));
   }
 
@@ -111,7 +111,7 @@
             TargetIdeInfo.builder().setLabel("//bar:OtherTest").setKind("java_test").build(),
             TargetIdeInfo.builder().setLabel("//foo:FooTest").setKind("java_test").build(),
             TargetIdeInfo.builder().setLabel("//bar/foo:FooTest").setKind("java_test").build());
-    Label match = TestTargetHeuristic.chooseTestTargetForSourceFile(source, targets, null);
+    Label match = TestTargetHeuristic.chooseTestTargetForSourceFile(project, source, targets, null);
     assertThat(match).isEqualTo(new Label("//foo:FooTest"));
   }
 }
diff --git a/base/tests/unittests/com/google/idea/blaze/base/run/TestSizeHeuristicTest.java b/base/tests/unittests/com/google/idea/blaze/base/run/TestSizeHeuristicTest.java
index 99ce084..34a41d4 100644
--- a/base/tests/unittests/com/google/idea/blaze/base/run/TestSizeHeuristicTest.java
+++ b/base/tests/unittests/com/google/idea/blaze/base/run/TestSizeHeuristicTest.java
@@ -51,7 +51,8 @@
             .setKind("java_test")
             .setTestInfo(TestIdeInfo.builder().setTestSize(TestSize.MEDIUM))
             .build();
-    assertThat(new TestSizeHeuristic().matchesSource(target, source, TestSize.MEDIUM)).isTrue();
+    assertThat(new TestSizeHeuristic().matchesSource(project, target, source, TestSize.MEDIUM))
+        .isTrue();
   }
 
   @Test
@@ -63,7 +64,8 @@
             .setKind("java_test")
             .setTestInfo(TestIdeInfo.builder().setTestSize(TestSize.MEDIUM))
             .build();
-    assertThat(new TestSizeHeuristic().matchesSource(target, source, TestSize.SMALL)).isFalse();
+    assertThat(new TestSizeHeuristic().matchesSource(project, target, source, TestSize.SMALL))
+        .isFalse();
   }
 
   @Test
@@ -75,7 +77,7 @@
             .setKind("java_test")
             .setTestInfo(TestIdeInfo.builder().setTestSize(TestSize.SMALL))
             .build();
-    assertThat(new TestSizeHeuristic().matchesSource(target, source, null)).isTrue();
+    assertThat(new TestSizeHeuristic().matchesSource(project, target, source, null)).isTrue();
 
     target =
         TargetIdeInfo.builder()
@@ -83,7 +85,7 @@
             .setKind("java_test")
             .setTestInfo(TestIdeInfo.builder().setTestSize(TestSize.MEDIUM))
             .build();
-    assertThat(new TestSizeHeuristic().matchesSource(target, source, null)).isFalse();
+    assertThat(new TestSizeHeuristic().matchesSource(project, target, source, null)).isFalse();
   }
 
   @Test
@@ -106,7 +108,8 @@
                 .setKind("java_test")
                 .setTestInfo(TestIdeInfo.builder().setTestSize(TestSize.ENORMOUS))
                 .build());
-    Label match = TestTargetHeuristic.chooseTestTargetForSourceFile(source, rules, TestSize.SMALL);
+    Label match =
+        TestTargetHeuristic.chooseTestTargetForSourceFile(project, source, rules, TestSize.SMALL);
     assertThat(match).isEqualTo(new Label("//foo:test1"));
   }
 
@@ -125,7 +128,8 @@
                 .setKind("java_test")
                 .setTestInfo(TestIdeInfo.builder().setTestSize(TestSize.SMALL))
                 .build());
-    Label match = TestTargetHeuristic.chooseTestTargetForSourceFile(source, rules, TestSize.SMALL);
+    Label match =
+        TestTargetHeuristic.chooseTestTargetForSourceFile(project, source, rules, TestSize.SMALL);
     assertThat(match).isEqualTo(new Label("//foo:test2"));
   }
 
@@ -149,7 +153,8 @@
                 .setKind("java_test")
                 .setTestInfo(TestIdeInfo.builder().setTestSize(TestSize.SMALL))
                 .build());
-    Label match = TestTargetHeuristic.chooseTestTargetForSourceFile(source, rules, TestSize.SMALL);
+    Label match =
+        TestTargetHeuristic.chooseTestTargetForSourceFile(project, source, rules, TestSize.SMALL);
     assertThat(match).isEqualTo(new Label("//foo:test2"));
   }
 }
diff --git a/base/tests/unittests/com/google/idea/blaze/base/run/TestTargetSourcesHeuristicTest.java b/base/tests/unittests/com/google/idea/blaze/base/run/TestTargetSourcesHeuristicTest.java
new file mode 100644
index 0000000..37ec7ae
--- /dev/null
+++ b/base/tests/unittests/com/google/idea/blaze/base/run/TestTargetSourcesHeuristicTest.java
@@ -0,0 +1,129 @@
+/*
+ * Copyright 2017 The Bazel Authors. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.idea.blaze.base.run;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.google.common.collect.ImmutableList;
+import com.google.idea.blaze.base.BlazeTestCase;
+import com.google.idea.blaze.base.ideinfo.ArtifactLocation;
+import com.google.idea.blaze.base.ideinfo.TargetIdeInfo;
+import com.google.idea.blaze.base.model.BlazeProjectData;
+import com.google.idea.blaze.base.model.MockBlazeProjectDataBuilder;
+import com.google.idea.blaze.base.model.MockBlazeProjectDataManager;
+import com.google.idea.blaze.base.model.primitives.Label;
+import com.google.idea.blaze.base.model.primitives.WorkspacePath;
+import com.google.idea.blaze.base.model.primitives.WorkspaceRoot;
+import com.google.idea.blaze.base.sync.data.BlazeProjectDataManager;
+import com.intellij.openapi.extensions.impl.ExtensionPointImpl;
+import java.io.File;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** Unit tests for {@link TestTargetSourcesHeuristic}. */
+@RunWith(JUnit4.class)
+public class TestTargetSourcesHeuristicTest extends BlazeTestCase {
+
+  private final WorkspaceRoot workspaceRoot = new WorkspaceRoot(new File("/"));
+
+  @Override
+  protected void initTest(Container applicationServices, Container projectServices) {
+    super.initTest(applicationServices, projectServices);
+
+    BlazeProjectData blazeProjectData = MockBlazeProjectDataBuilder.builder(workspaceRoot).build();
+    projectServices.register(
+        BlazeProjectDataManager.class, new MockBlazeProjectDataManager(blazeProjectData));
+
+    ExtensionPointImpl<TestTargetHeuristic> ep =
+        registerExtensionPoint(TestTargetHeuristic.EP_NAME, TestTargetHeuristic.class);
+    ep.registerExtension(new TestTargetSourcesHeuristic());
+  }
+
+  @Test
+  public void testPredicateNoSources() {
+    File source = workspaceRoot.fileForPath(new WorkspacePath("java/com/foo/FooTest.java"));
+    TargetIdeInfo target =
+        TargetIdeInfo.builder().setLabel("//foo:test").setKind("java_test").build();
+    assertThat(new TestTargetSourcesHeuristic().matchesSource(project, target, source, null))
+        .isFalse();
+  }
+
+  @Test
+  public void testPredicateNoMatchingSource() {
+    File source = workspaceRoot.fileForPath(new WorkspacePath("java/com/foo/FooTest.java"));
+    TargetIdeInfo target =
+        TargetIdeInfo.builder()
+            .setLabel("//foo:test")
+            .setKind("java_test")
+            .addSource(sourceRoot("java/com/bar/OtherTest.java"))
+            .build();
+    assertThat(new TestTargetSourcesHeuristic().matchesSource(project, target, source, null))
+        .isFalse();
+  }
+
+  @Test
+  public void testPredicateMatchingSource() {
+    File source = workspaceRoot.fileForPath(new WorkspacePath("java/com/foo/FooTest.java"));
+    TargetIdeInfo target =
+        TargetIdeInfo.builder()
+            .setLabel("//foo:test")
+            .setKind("java_test")
+            .addSource(sourceRoot("java/com/bar/OtherTest.java"))
+            .addSource(sourceRoot("java/com/foo/FooTest.java"))
+            .build();
+    assertThat(new TestTargetSourcesHeuristic().matchesSource(project, target, source, null))
+        .isTrue();
+  }
+
+  @Test
+  public void testFilterNoMatchesFallBackToFirstRule() throws Exception {
+    File source = workspaceRoot.fileForPath(new WorkspacePath("java/com/foo/FooTest.java"));
+    ImmutableList<TargetIdeInfo> rules =
+        ImmutableList.of(
+            TargetIdeInfo.builder()
+                .setLabel("//foo:test1")
+                .setKind("java_test")
+                .addSource(sourceRoot("java/com/bar/OtherTest.java"))
+                .build(),
+            TargetIdeInfo.builder().setLabel("//foo:test2").setKind("java_test").build());
+    Label match = TestTargetHeuristic.chooseTestTargetForSourceFile(project, source, rules, null);
+    assertThat(match).isEqualTo(new Label("//foo:test1"));
+  }
+
+  @Test
+  public void testFilterOneMatch() throws Exception {
+    File source = workspaceRoot.fileForPath(new WorkspacePath("java/com/foo/FooTest.java"));
+    ImmutableList<TargetIdeInfo> rules =
+        ImmutableList.of(
+            TargetIdeInfo.builder()
+                .setLabel("//foo:test1")
+                .setKind("java_test")
+                .addSource(sourceRoot("java/com/bar/OtherTest.java"))
+                .build(),
+            TargetIdeInfo.builder()
+                .setLabel("//foo:test2")
+                .setKind("java_test")
+                .addSource(sourceRoot("java/com/foo/FooTest.java"))
+                .build());
+    Label match = TestTargetHeuristic.chooseTestTargetForSourceFile(project, source, rules, null);
+    assertThat(match).isEqualTo(new Label("//foo:test2"));
+  }
+
+  private static ArtifactLocation sourceRoot(String relativePath) {
+    return ArtifactLocation.builder().setRelativePath(relativePath).setIsSource(true).build();
+  }
+}
diff --git a/base/tests/unittests/com/google/idea/blaze/base/run/smrunner/BlazeXmlSchemaTest.java b/base/tests/unittests/com/google/idea/blaze/base/run/smrunner/BlazeXmlSchemaTest.java
index 5c7dc35..2ed69f5 100644
--- a/base/tests/unittests/com/google/idea/blaze/base/run/smrunner/BlazeXmlSchemaTest.java
+++ b/base/tests/unittests/com/google/idea/blaze/base/run/smrunner/BlazeXmlSchemaTest.java
@@ -24,6 +24,7 @@
 import java.io.InputStream;
 import java.nio.charset.StandardCharsets;
 import java.util.List;
+import java.util.stream.Collectors;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.junit.runners.JUnit4;
@@ -68,4 +69,51 @@
     TestSuite parsed = BlazeXmlSchema.parse(stream);
     assertThat(parsed).isNotNull();
   }
+
+  @Test
+  public void testMergeShardedTests() {
+    TestSuite shard1 =
+        parseXml(
+            "<?xml version='1.0' encoding='UTF-8'?>",
+            "<testsuites>",
+            "  <testsuite name='com.google.ConfigTest' time='10' tests='2' failures='1'>",
+            "    <testcase name='testCase1' time='2.1' status='run' result='completed'/>",
+            "    <testcase name='testCase2' time='7.9' status='run' result='completed'>",
+            "      <failure message='failed'/>",
+            "    </testcase>",
+            "  </testsuite>",
+            "</testsuites>");
+    TestSuite shard2 =
+        parseXml(
+            "<?xml version='1.0' encoding='UTF-8'?>",
+            "<testsuites>",
+            "  <testsuite name='com.google.ConfigTest' time='5' tests='2' failures='1'>",
+            "    <testcase name='testCase3' time='1' status='run' result='completed'/>",
+            "    <testcase name='testCase4' time='4' status='run' result='completed'>",
+            "      <failure message='failed'/>",
+            "    </testcase>",
+            "  </testsuite>",
+            "</testsuites>");
+    TestSuite mergedOuter = BlazeXmlSchema.mergeSuites(ImmutableList.of(shard1, shard2));
+    assertThat(mergedOuter.testSuites).hasSize(1);
+    TestSuite mergedInner = mergedOuter.testSuites.get(0).testSuites.get(0);
+    assertThat(mergedInner.name).isEqualTo("com.google.ConfigTest");
+    assertThat(mergedInner.time).isEqualTo(15d);
+    assertThat(mergedInner.tests).isEqualTo(4);
+    assertThat(mergedInner.failures).isEqualTo(2);
+    assertThat(mergedInner.testCases).hasSize(4);
+    assertThat(
+            mergedInner
+                .testCases
+                .stream()
+                .map(testCase -> testCase.name)
+                .collect(Collectors.toList()))
+        .containsExactly("testCase1", "testCase2", "testCase3", "testCase4");
+  }
+
+  private static TestSuite parseXml(String... lines) {
+    InputStream stream =
+        new ByteArrayInputStream(Joiner.on('\n').join(lines).getBytes(StandardCharsets.UTF_8));
+    return BlazeXmlSchema.parse(stream);
+  }
 }
diff --git a/base/tests/unittests/com/google/idea/blaze/base/run/state/BlazeCommandRunConfigurationCommonStateTest.java b/base/tests/unittests/com/google/idea/blaze/base/run/state/BlazeCommandRunConfigurationCommonStateTest.java
index 5598b49..e94bedf 100644
--- a/base/tests/unittests/com/google/idea/blaze/base/run/state/BlazeCommandRunConfigurationCommonStateTest.java
+++ b/base/tests/unittests/com/google/idea/blaze/base/run/state/BlazeCommandRunConfigurationCommonStateTest.java
@@ -47,8 +47,7 @@
     super.initTest(applicationServices, projectServices);
 
     applicationServices.register(UISettings.class, new UISettings());
-    projectServices.register(
-        BlazeImportSettingsManager.class, new BlazeImportSettingsManager(project));
+    projectServices.register(BlazeImportSettingsManager.class, new BlazeImportSettingsManager());
     BlazeImportSettingsManager.getInstance(getProject()).setImportSettings(DUMMY_IMPORT_SETTINGS);
 
     registerExtensionPoint(DistributedExecutorSupport.EP_NAME, DistributedExecutorSupport.class);
diff --git a/base/tests/unittests/com/google/idea/blaze/base/sync/workspace/ArtifactLocationDecoderTest.java b/base/tests/unittests/com/google/idea/blaze/base/sync/workspace/ArtifactLocationDecoderTest.java
index a7c7816..eb02cba 100644
--- a/base/tests/unittests/com/google/idea/blaze/base/sync/workspace/ArtifactLocationDecoderTest.java
+++ b/base/tests/unittests/com/google/idea/blaze/base/sync/workspace/ArtifactLocationDecoderTest.java
@@ -17,18 +17,10 @@
 
 import static com.google.common.truth.Truth.assertThat;
 
-import com.google.common.collect.ImmutableList;
-import com.google.common.collect.Lists;
-import com.google.common.collect.Sets;
 import com.google.idea.blaze.base.BlazeTestCase;
 import com.google.idea.blaze.base.ideinfo.ArtifactLocation;
-import com.google.idea.blaze.base.io.FileAttributeProvider;
 import com.google.idea.blaze.base.model.primitives.ExecutionRootPath;
-import com.google.idea.blaze.base.model.primitives.WorkspaceRoot;
 import java.io.File;
-import java.util.List;
-import java.util.Set;
-import org.jetbrains.annotations.NotNull;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.junit.runners.JUnit4;
@@ -40,77 +32,6 @@
   private static final String EXECUTION_ROOT = "/path/to/_blaze_user/1234bf129e/root";
   private static final String OUTPUT_BASE = "/path/to/_blaze_user/1234bf129e";
 
-  static class MockFileAttributeProvider extends FileAttributeProvider {
-    final Set<File> files = Sets.newHashSet();
-
-    void addFiles(@NotNull File... files) {
-      this.files.addAll(Lists.newArrayList(files));
-    }
-
-    @Override
-    public boolean exists(@NotNull File file) {
-      return files.contains(file);
-    }
-  }
-
-  private MockFileAttributeProvider fileChecker;
-
-  @Override
-  protected void initTest(
-      @NotNull Container applicationServices, @NotNull Container projectServices) {
-    super.initTest(applicationServices, projectServices);
-
-    fileChecker = new MockFileAttributeProvider();
-    applicationServices.register(FileAttributeProvider.class, fileChecker);
-  }
-
-  @Test
-  public void testManualPackagePaths() throws Exception {
-    List<File> packagePaths =
-        ImmutableList.of(
-            new File("/path/to"),
-            new File("/path/to/READONLY/root"),
-            new File("/path/to/CUSTOM/root"));
-
-    BlazeRoots blazeRoots =
-        new BlazeRoots(
-            new File(EXECUTION_ROOT),
-            packagePaths,
-            new ExecutionRootPath("root/blaze-out/crosstool/bin"),
-            new ExecutionRootPath("root/blaze-out/crosstool/genfiles"),
-            new File(OUTPUT_BASE));
-
-    fileChecker.addFiles(
-        new File("/path/to/com/google/Bla.java"),
-        new File("/path/to/READONLY/root/com/google/Foo.java"),
-        new File("/path/to/CUSTOM/root/com/other/Test.java"));
-
-    ArtifactLocationDecoder decoder =
-        new ArtifactLocationDecoderImpl(
-            blazeRoots,
-            new WorkspacePathResolverImpl(
-                new WorkspaceRoot(new File("/path/to/root")), blazeRoots));
-
-    ArtifactLocation blah =
-        ArtifactLocation.builder().setRelativePath("com/google/Bla.java").setIsSource(true).build();
-    assertThat(decoder.decode(blah).getPath()).isEqualTo("/path/to/com/google/Bla.java");
-
-    ArtifactLocation foo =
-        ArtifactLocation.builder().setRelativePath("com/google/Foo.java").setIsSource(true).build();
-    assertThat(decoder.decode(foo).getPath())
-        .isEqualTo("/path/to/READONLY/root/com/google/Foo.java");
-
-    ArtifactLocation test =
-        ArtifactLocation.builder().setRelativePath("com/other/Test.java").setIsSource(true).build();
-    assertThat(decoder.decode(test).getPath())
-        .isEqualTo("/path/to/CUSTOM/root/com/other/Test.java");
-
-    ArtifactLocation.Builder temp =
-        ArtifactLocation.builder().setRelativePath("third_party/other/Temp.java").setIsSource(true);
-    assertThat(decoder.decode(temp.build()).getPath())
-        .isEqualTo("/path/to/third_party/other/Temp.java");
-  }
-
   @Test
   public void testGeneratedArtifact() throws Exception {
     ArtifactLocation artifactLocation =
@@ -124,7 +45,6 @@
         new ArtifactLocationDecoderImpl(
             new BlazeRoots(
                 new File(EXECUTION_ROOT),
-                ImmutableList.of(new File("/path/to/root")),
                 new ExecutionRootPath("root/blaze-out/crosstool/bin"),
                 new ExecutionRootPath("root/blaze-out/crosstool/genfiles"),
                 new File(OUTPUT_BASE)),
@@ -147,7 +67,6 @@
         new ArtifactLocationDecoderImpl(
             new BlazeRoots(
                 new File(EXECUTION_ROOT),
-                ImmutableList.of(new File("/path/to/root")),
                 new ExecutionRootPath("root/blaze-out/crosstool/bin"),
                 new ExecutionRootPath("root/blaze-out/crosstool/genfiles"),
                 new File(OUTPUT_BASE)),
diff --git a/base/tests/unittests/com/google/idea/blaze/base/sync/workspace/ExecutionRootPathResolverTest.java b/base/tests/unittests/com/google/idea/blaze/base/sync/workspace/ExecutionRootPathResolverTest.java
index f880353..afba81b 100644
--- a/base/tests/unittests/com/google/idea/blaze/base/sync/workspace/ExecutionRootPathResolverTest.java
+++ b/base/tests/unittests/com/google/idea/blaze/base/sync/workspace/ExecutionRootPathResolverTest.java
@@ -19,9 +19,13 @@
 
 import com.google.common.collect.ImmutableList;
 import com.google.idea.blaze.base.BlazeTestCase;
+import com.google.idea.blaze.base.bazel.BazelBuildSystemProvider;
+import com.google.idea.blaze.base.bazel.BuildSystemProvider;
 import com.google.idea.blaze.base.model.primitives.ExecutionRootPath;
 import com.google.idea.blaze.base.model.primitives.WorkspacePath;
 import com.google.idea.blaze.base.model.primitives.WorkspaceRoot;
+import com.google.idea.blaze.base.settings.Blaze.BuildSystem;
+import com.intellij.openapi.extensions.ExtensionPoint;
 import java.io.File;
 import org.junit.Test;
 import org.junit.runner.RunWith;
@@ -32,19 +36,23 @@
 public class ExecutionRootPathResolverTest extends BlazeTestCase {
 
   private static final WorkspaceRoot WORKSPACE_ROOT = new WorkspaceRoot(new File("/path/to/root"));
-  private static final String EXECUTION_ROOT = "/path/to/_blaze_user/1234bf129e/root";
+  private static final String EXECUTION_ROOT = "/path/to/_bazel_user/1234bf129e/root";
 
-  private static final BlazeRoots BLAZE_ROOTS =
-      new BlazeRoots(
-          new File(EXECUTION_ROOT),
-          ImmutableList.of(WORKSPACE_ROOT.directory()),
-          new ExecutionRootPath("blaze-out/crosstool/bin"),
-          new ExecutionRootPath("blaze-out/crosstool/genfiles"),
-          null);
+  private ExecutionRootPathResolver pathResolver;
 
-  private final ExecutionRootPathResolver pathResolver =
-      new ExecutionRootPathResolver(
-          BLAZE_ROOTS, new WorkspacePathResolverImpl(WORKSPACE_ROOT, BLAZE_ROOTS));
+  @Override
+  protected void initTest(Container applicationServices, Container projectServices) {
+    ExtensionPoint<BuildSystemProvider> extensionPoint =
+        registerExtensionPoint(BuildSystemProvider.EP_NAME, BuildSystemProvider.class);
+    extensionPoint.registerExtension(new BazelBuildSystemProvider());
+
+    pathResolver =
+        new ExecutionRootPathResolver(
+            BuildSystem.Bazel,
+            WORKSPACE_ROOT,
+            new File(EXECUTION_ROOT),
+            new WorkspacePathResolverImpl(WORKSPACE_ROOT));
+  }
 
   @Test
   public void testExternalWorkspacePathRelativeToExecRoot() {
@@ -57,9 +65,9 @@
   public void testGenfilesPathRelativeToExecRoot() {
     ImmutableList<File> files =
         pathResolver.resolveToIncludeDirectories(
-            new ExecutionRootPath("blaze-out/crosstool/genfiles/res/normal"));
+            new ExecutionRootPath("bazel-out/crosstool/genfiles/res/normal"));
     assertThat(files)
-        .containsExactly(new File(EXECUTION_ROOT, "blaze-out/crosstool/genfiles/res/normal"));
+        .containsExactly(new File(EXECUTION_ROOT, "bazel-out/crosstool/genfiles/res/normal"));
   }
 
   @Test
@@ -68,4 +76,14 @@
         pathResolver.resolveToIncludeDirectories(new ExecutionRootPath("tools/fast"));
     assertThat(files).containsExactly(WORKSPACE_ROOT.fileForPath(new WorkspacePath("tools/fast")));
   }
+
+  @Test
+  public void testGenfilesPathWithDifferentConfigSettingStillResolves() {
+    ImmutableList<File> files =
+        pathResolver.resolveToIncludeDirectories(
+            new ExecutionRootPath("bazel-out/arm-linux-fastbuild/genfiles/res/normal"));
+    assertThat(files)
+        .containsExactly(
+            new File(EXECUTION_ROOT, "bazel-out/arm-linux-fastbuild/genfiles/res/normal"));
+  }
 }
diff --git a/base/tests/unittests/com/google/idea/blaze/base/sync/workspace/WorkspacePathResolverImplTest.java b/base/tests/unittests/com/google/idea/blaze/base/sync/workspace/WorkspacePathResolverImplTest.java
index 282ea53..446d6ba 100644
--- a/base/tests/unittests/com/google/idea/blaze/base/sync/workspace/WorkspacePathResolverImplTest.java
+++ b/base/tests/unittests/com/google/idea/blaze/base/sync/workspace/WorkspacePathResolverImplTest.java
@@ -19,7 +19,6 @@
 
 import com.google.common.collect.ImmutableList;
 import com.google.idea.blaze.base.BlazeTestCase;
-import com.google.idea.blaze.base.model.primitives.ExecutionRootPath;
 import com.google.idea.blaze.base.model.primitives.WorkspacePath;
 import com.google.idea.blaze.base.model.primitives.WorkspaceRoot;
 import java.io.File;
@@ -31,20 +30,10 @@
 @RunWith(JUnit4.class)
 public class WorkspacePathResolverImplTest extends BlazeTestCase {
   private static final WorkspaceRoot WORKSPACE_ROOT = new WorkspaceRoot(new File("/path/to/root"));
-  private static final String EXECUTION_ROOT = "/path/to/_blaze_user/1234bf129e/root";
-
-  private static final BlazeRoots BLAZE_CITC_ROOTS =
-      new BlazeRoots(
-          new File(EXECUTION_ROOT),
-          ImmutableList.of(WORKSPACE_ROOT.directory()),
-          new ExecutionRootPath("blaze-out/crosstool/bin"),
-          new ExecutionRootPath("blaze-out/crosstool/genfiles"),
-          null);
 
   @Test
   public void testResolveToIncludeDirectories() {
-    WorkspacePathResolver workspacePathResolver =
-        new WorkspacePathResolverImpl(WORKSPACE_ROOT, BLAZE_CITC_ROOTS);
+    WorkspacePathResolver workspacePathResolver = new WorkspacePathResolverImpl(WORKSPACE_ROOT);
     ImmutableList<File> files =
         workspacePathResolver.resolveToIncludeDirectories(new WorkspacePath("tools/fast"));
     assertThat(files).containsExactly(new File("/path/to/root/tools/fast"));
@@ -52,11 +41,18 @@
 
   @Test
   public void testResolveToIncludeDirectoriesForExecRootPath() {
-    WorkspacePathResolver workspacePathResolver =
-        new WorkspacePathResolverImpl(WORKSPACE_ROOT, BLAZE_CITC_ROOTS);
+    WorkspacePathResolver workspacePathResolver = new WorkspacePathResolverImpl(WORKSPACE_ROOT);
     ImmutableList<File> files =
         workspacePathResolver.resolveToIncludeDirectories(
             new WorkspacePath("blaze-out/crosstool/bin/tools/fast"));
     assertThat(files).containsExactly(new File("/path/to/root/blaze-out/crosstool/bin/tools/fast"));
   }
+
+  @Test
+  public void testResolveToFile() {
+    WorkspacePathResolver workspacePathResolver = new WorkspacePathResolverImpl(WORKSPACE_ROOT);
+    WorkspacePath relativePath = new WorkspacePath("third_party/tools");
+    assertThat(workspacePathResolver.resolveToFile(relativePath))
+        .isEqualTo(WORKSPACE_ROOT.fileForPath(relativePath));
+  }
 }
diff --git a/base/tests/utils/integration/com/google/idea/blaze/base/run/producer/BlazeRunConfigurationProducerTestCase.java b/base/tests/utils/integration/com/google/idea/blaze/base/run/producer/BlazeRunConfigurationProducerTestCase.java
new file mode 100644
index 0000000..0b34e07
--- /dev/null
+++ b/base/tests/utils/integration/com/google/idea/blaze/base/run/producer/BlazeRunConfigurationProducerTestCase.java
@@ -0,0 +1,138 @@
+/*
+ * Copyright 2017 The Bazel Authors. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.idea.blaze.base.run.producer;
+
+import com.google.idea.blaze.base.BlazeIntegrationTestCase;
+import com.google.idea.blaze.base.EditorTestHelper;
+import com.google.idea.blaze.base.command.BlazeCommandName;
+import com.google.idea.blaze.base.ideinfo.ArtifactLocation;
+import com.google.idea.blaze.base.model.MockBlazeProjectDataBuilder;
+import com.google.idea.blaze.base.model.MockBlazeProjectDataManager;
+import com.google.idea.blaze.base.model.primitives.WorkspacePath;
+import com.google.idea.blaze.base.run.BlazeCommandRunConfiguration;
+import com.google.idea.blaze.base.run.state.BlazeCommandRunConfigurationCommonState;
+import com.google.idea.blaze.base.sync.SyncCache;
+import com.google.idea.blaze.base.sync.data.BlazeProjectDataManager;
+import com.intellij.execution.Location;
+import com.intellij.execution.PsiLocation;
+import com.intellij.execution.RunnerAndConfigurationSettings;
+import com.intellij.execution.actions.ConfigurationContext;
+import com.intellij.execution.configurations.RunConfiguration;
+import com.intellij.ide.DataManager;
+import com.intellij.idea.CommandLineApplication.MyDataManagerImpl;
+import com.intellij.openapi.actionSystem.CommonDataKeys;
+import com.intellij.openapi.actionSystem.DataContext;
+import com.intellij.openapi.actionSystem.LangDataKeys;
+import com.intellij.openapi.module.ModuleUtil;
+import com.intellij.openapi.util.Key;
+import com.intellij.psi.PsiElement;
+import com.intellij.psi.PsiFile;
+import com.intellij.testFramework.MapDataContext;
+import javax.annotation.Nullable;
+import org.junit.After;
+import org.junit.Before;
+
+/** Run configuration producer integration test base */
+public class BlazeRunConfigurationProducerTestCase extends BlazeIntegrationTestCase {
+
+  protected EditorTestHelper editorTest;
+
+  @Before
+  public final void doSetup() {
+    BlazeProjectDataManager mockProjectDataManager =
+        new MockBlazeProjectDataManager(MockBlazeProjectDataBuilder.builder(workspaceRoot).build());
+    registerProjectService(BlazeProjectDataManager.class, mockProjectDataManager);
+    editorTest = new EditorTestHelper(getProject(), testFixture);
+
+    // IntelliJ replaces the normal DataManager with a mock version in headless environments.
+    // We rely on a functional DataManager in run configuration tests to recognize when multiple
+    // psi elements are selected.
+    DataManager dataManager =
+        new MyDataManagerImpl() {
+          DataContext dataContext;
+
+          @Override
+          public <T> void saveInDataContext(
+              DataContext dataContext, Key<T> dataKey, @Nullable T data) {
+            this.dataContext = dataContext;
+            super.saveInDataContext(dataContext, dataKey, data);
+          }
+
+          @Override
+          public DataContext getDataContext() {
+            return dataContext != null ? dataContext : super.getDataContext();
+          }
+        };
+    registerApplicationComponent(DataManager.class, dataManager);
+  }
+
+  @After
+  public final void doTearDown() {
+    SyncCache.getInstance(getProject()).clear();
+  }
+
+  protected PsiFile createAndIndexFile(WorkspacePath path, String... contents) {
+    PsiFile file = workspace.createPsiFile(path, contents);
+    editorTest.openFileInEditor(file); // open file to trigger update of indices
+    return file;
+  }
+
+  @Nullable
+  protected static String getTestFilterContents(BlazeCommandRunConfiguration config) {
+    BlazeCommandRunConfigurationCommonState handlerState =
+        config.getHandlerStateIfType(BlazeCommandRunConfigurationCommonState.class);
+    return handlerState != null ? handlerState.getTestFilterFlag() : null;
+  }
+
+  @Nullable
+  protected static BlazeCommandName getCommandType(BlazeCommandRunConfiguration config) {
+    BlazeCommandRunConfigurationCommonState handlerState =
+        config.getHandlerStateIfType(BlazeCommandRunConfigurationCommonState.class);
+    return handlerState != null ? handlerState.getCommand() : null;
+  }
+
+  protected ConfigurationContext createContextFromPsi(PsiElement element) {
+    final MapDataContext dataContext = new MapDataContext();
+    dataContext.put(CommonDataKeys.PROJECT, getProject());
+    dataContext.put(LangDataKeys.MODULE, ModuleUtil.findModuleForPsiElement(element));
+    dataContext.put(Location.DATA_KEY, PsiLocation.fromPsiElement(element));
+    return ConfigurationContext.getFromContext(dataContext);
+  }
+
+  protected ConfigurationContext createContextFromMultipleElements(PsiElement[] elements) {
+    final MapDataContext dataContext = new MapDataContext();
+    dataContext.put(CommonDataKeys.PROJECT, getProject());
+    dataContext.put(LangDataKeys.MODULE, ModuleUtil.findModuleForPsiElement(elements[0]));
+    dataContext.put(Location.DATA_KEY, PsiLocation.fromPsiElement(elements[0]));
+    dataContext.put(LangDataKeys.PSI_ELEMENT_ARRAY, elements);
+    return ConfigurationContext.getFromContext(dataContext);
+  }
+
+  @Nullable
+  protected RunConfiguration createConfigurationFromLocation(PsiFile psiFile) {
+    MapDataContext dataContext = new MapDataContext();
+    dataContext.put(CommonDataKeys.PROJECT, getProject());
+    dataContext.put(LangDataKeys.MODULE, ModuleUtil.findModuleForPsiElement(psiFile));
+    dataContext.put(Location.DATA_KEY, PsiLocation.fromPsiElement(psiFile));
+    RunnerAndConfigurationSettings settings =
+        ConfigurationContext.getFromContext(dataContext).getConfiguration();
+    return settings != null ? settings.getConfiguration() : null;
+  }
+
+  protected static ArtifactLocation sourceRoot(String relativePath) {
+    return ArtifactLocation.builder().setRelativePath(relativePath).setIsSource(true).build();
+  }
+}
diff --git a/base/tests/utils/integration/com/google/idea/blaze/base/sync/BlazeSyncIntegrationTestCase.java b/base/tests/utils/integration/com/google/idea/blaze/base/sync/BlazeSyncIntegrationTestCase.java
index 53c6b53..e6b8453 100644
--- a/base/tests/utils/integration/com/google/idea/blaze/base/sync/BlazeSyncIntegrationTestCase.java
+++ b/base/tests/utils/integration/com/google/idea/blaze/base/sync/BlazeSyncIntegrationTestCase.java
@@ -55,6 +55,7 @@
 import com.google.idea.blaze.base.sync.workspace.WorkspacePathResolverImpl;
 import com.google.idea.blaze.base.vcs.BlazeVcsHandler;
 import com.intellij.ide.IdeEventQueue;
+import com.intellij.openapi.module.Module;
 import com.intellij.openapi.project.Project;
 import com.intellij.openapi.roots.ContentEntry;
 import com.intellij.openapi.roots.ModifiableRootModel;
@@ -87,6 +88,26 @@
   protected BlazeContext context;
 
   private ImmutableList<ContentEntry> workspaceContentEntries = ImmutableList.of();
+  private Map<String, ModifiableRootModel> modules = Maps.newHashMap();
+
+  private class MockModuleEditor extends ModuleEditorImpl {
+    public MockModuleEditor(Project project, BlazeImportSettings importSettings) {
+      super(project, importSettings);
+    }
+
+    @Override
+    public void commit() {
+      // don't commit module changes,
+      // and make sure they're properly disposed when the test is finished
+      for (ModifiableRootModel model : modules.values()) {
+        Disposer.register(getTestRootDisposable(), model::dispose);
+        if (model.getModule().getName().equals(BlazeDataStorage.WORKSPACE_MODULE_NAME)) {
+          workspaceContentEntries = ImmutableList.copyOf(model.getContentEntries());
+        }
+      }
+      BlazeSyncIntegrationTestCase.this.modules = modules;
+    }
+  }
 
   @Before
   public void doSetup() throws Exception {
@@ -98,27 +119,7 @@
     registerExtension(BlazeVcsHandler.EP_NAME, vcsHandler);
     registerApplicationService(BlazeInfo.class, blazeInfoData);
     registerApplicationService(BlazeIdeInterface.class, blazeIdeInterface);
-    registerApplicationService(
-        ModuleEditorProvider.class,
-        new ModuleEditorProvider() {
-          @Override
-          public ModuleEditorImpl getModuleEditor(
-              Project project, BlazeImportSettings importSettings) {
-            return new ModuleEditorImpl(project, importSettings) {
-              @Override
-              public void commit() {
-                // don't commit module changes,
-                // and make sure they're properly disposed when the test is finished
-                for (ModifiableRootModel model : modules.values()) {
-                  Disposer.register(getTestRootDisposable(), model::dispose);
-                  if (model.getModule().getName().equals(BlazeDataStorage.WORKSPACE_MODULE_NAME)) {
-                    workspaceContentEntries = ImmutableList.copyOf(model.getContentEntries());
-                  }
-                }
-              }
-            };
-          }
-        });
+    registerApplicationService(ModuleEditorProvider.class, MockModuleEditor::new);
 
     errorCollector = new ErrorCollector();
     context = new BlazeContext();
@@ -145,6 +146,12 @@
     return workspaceContentEntries;
   }
 
+  /** The modules created during sync */
+  protected Module getModuleCreatedDuringSync(String module) {
+    ModifiableRootModel modifiableRootModel = modules.get(module);
+    return modifiableRootModel != null ? modifiableRootModel.getModule() : null;
+  }
+
   /** Search the workspace module's {@link ContentEntry}s for one with the given file. */
   @Nullable
   protected ContentEntry findContentEntry(VirtualFile root) {
@@ -268,7 +275,7 @@
     @Override
     public ListenableFuture<String> runBlazeInfo(
         @Nullable BlazeContext context,
-        BuildSystem buildSystem,
+        String binaryPath,
         WorkspaceRoot workspaceRoot,
         List<String> blazeFlags,
         String key) {
@@ -278,7 +285,7 @@
     @Override
     public ListenableFuture<byte[]> runBlazeInfoGetBytes(
         @Nullable BlazeContext context,
-        BuildSystem buildSystem,
+        String binaryPath,
         WorkspaceRoot workspaceRoot,
         List<String> blazeFlags,
         String key) {
@@ -288,7 +295,7 @@
     @Override
     public ListenableFuture<ImmutableMap<String, String>> runBlazeInfo(
         @Nullable BlazeContext context,
-        BuildSystem buildSystem,
+        String binaryPath,
         WorkspaceRoot workspaceRoot,
         List<String> blazeFlags) {
       return Futures.immediateFuture(ImmutableMap.copyOf(results));
diff --git a/base/tests/utils/unit/com/google/idea/blaze/base/TestUtils.java b/base/tests/utils/unit/com/google/idea/blaze/base/TestUtils.java
index f040cf2..e0f2d47 100644
--- a/base/tests/utils/unit/com/google/idea/blaze/base/TestUtils.java
+++ b/base/tests/utils/unit/com/google/idea/blaze/base/TestUtils.java
@@ -37,8 +37,8 @@
 import java.io.Serializable;
 import java.util.concurrent.Callable;
 import java.util.concurrent.Future;
+import javax.annotation.Nullable;
 import org.jetbrains.annotations.NotNull;
-import org.jetbrains.annotations.Nullable;
 import org.picocontainer.PicoContainer;
 
 /** Test utilities. */
diff --git a/base/tests/utils/unit/com/google/idea/blaze/base/model/MockBlazeProjectDataBuilder.java b/base/tests/utils/unit/com/google/idea/blaze/base/model/MockBlazeProjectDataBuilder.java
index 25bd08d..28b4b9c 100644
--- a/base/tests/utils/unit/com/google/idea/blaze/base/model/MockBlazeProjectDataBuilder.java
+++ b/base/tests/utils/unit/com/google/idea/blaze/base/model/MockBlazeProjectDataBuilder.java
@@ -15,7 +15,6 @@
  */
 package com.google.idea.blaze.base.model;
 
-import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableMap;
 import com.google.common.collect.ImmutableMultimap;
 import com.google.common.collect.ImmutableSet;
@@ -30,11 +29,12 @@
 import com.google.idea.blaze.base.sync.workspace.BlazeRoots;
 import com.google.idea.blaze.base.sync.workspace.WorkspacePathResolver;
 import com.google.idea.blaze.base.sync.workspace.WorkspacePathResolverImpl;
+import java.io.File;
 
 /**
- * Use to build mock praject data for tests.
+ * Use to build mock project data for tests.
  *
- * <p>For any data you don't supply, the builder make a best-effort attempt to create default
+ * <p>For any data you don't supply, the builder makes a best-effort attempt to create default
  * objects using whatever data you have supplied if applicable.
  */
 public class MockBlazeProjectDataBuilder {
@@ -51,7 +51,7 @@
   private SyncState syncState;
   private ImmutableMultimap<TargetKey, TargetKey> reverseDependencies;
 
-  public MockBlazeProjectDataBuilder(WorkspaceRoot workspaceRoot) {
+  private MockBlazeProjectDataBuilder(WorkspaceRoot workspaceRoot) {
     this.workspaceRoot = workspaceRoot;
   }
 
@@ -122,8 +122,7 @@
         this.blazeRoots != null
             ? this.blazeRoots
             : new BlazeRoots(
-                null,
-                ImmutableList.of(workspaceRoot.directory()),
+                new File(workspaceRoot.directory().getParentFile(), "exec_root"),
                 new ExecutionRootPath("bin"),
                 new ExecutionRootPath("gen"),
                 null);
@@ -132,7 +131,7 @@
     WorkspacePathResolver workspacePathResolver =
         this.workspacePathResolver != null
             ? this.workspacePathResolver
-            : new WorkspacePathResolverImpl(workspaceRoot, blazeRoots);
+            : new WorkspacePathResolverImpl(workspaceRoot);
     ArtifactLocationDecoder artifactLocationDecoder =
         this.artifactLocationDecoder != null
             ? this.artifactLocationDecoder
diff --git a/clwb/BUILD b/clwb/BUILD
index f1048b9..fbd119b 100644
--- a/clwb/BUILD
+++ b/clwb/BUILD
@@ -49,6 +49,7 @@
         "//common/experiments",
         "//cpp",
         "//intellij_platform_sdk:plugin_api",
+        "//sdkcompat",
         "@jsr305_annotations//jar",
     ],
 )
diff --git a/clwb/src/META-INF/clwb.xml b/clwb/src/META-INF/clwb.xml
index 222ef38..32e7b0a 100644
--- a/clwb/src/META-INF/clwb.xml
+++ b/clwb/src/META-INF/clwb.xml
@@ -50,6 +50,7 @@
     <SyncPlugin implementation="com.google.idea.blaze.clwb.sync.BlazeCLionSyncPlugin"/>
     <BlazeCommandRunConfigurationHandlerProvider implementation="com.google.idea.blaze.clwb.run.BlazeCidrRunConfigurationHandlerProvider" order="first"/>
     <RunConfigurationFactory implementation="com.google.idea.blaze.clwb.run.BlazeCidrDebuggableConfigurationFactory"/>
+    <BlazeTestEventsHandler implementation="com.google.idea.blaze.clwb.run.test.BlazeCidrTestEventsHandler"/>
   </extensions>
 
   <actions>
diff --git a/clwb/src/com/google/idea/blaze/clwb/problemsview/BlazeProblemsViewConsole.java b/clwb/src/com/google/idea/blaze/clwb/problemsview/BlazeProblemsViewConsole.java
index 543f346..725dde4 100644
--- a/clwb/src/com/google/idea/blaze/clwb/problemsview/BlazeProblemsViewConsole.java
+++ b/clwb/src/com/google/idea/blaze/clwb/problemsview/BlazeProblemsViewConsole.java
@@ -36,7 +36,7 @@
 import com.intellij.ui.content.Content;
 import com.intellij.ui.content.ContentFactory;
 import com.intellij.util.ArrayUtil;
-import com.intellij.util.concurrency.SequentialTaskExecutor;
+import com.intellij.util.concurrency.BoundedTaskExecutor;
 import com.intellij.util.ui.MessageCategory;
 import com.intellij.util.ui.UIUtil;
 import java.util.ArrayList;
@@ -44,6 +44,7 @@
 import java.util.List;
 import java.util.StringTokenizer;
 import java.util.UUID;
+import java.util.concurrent.ExecutorService;
 import javax.swing.Icon;
 import org.jetbrains.annotations.NotNull;
 import org.jetbrains.annotations.Nullable;
@@ -58,8 +59,8 @@
       EnumSet.allOf(ErrorTreeElementKind.class);
 
   private final ProblemsViewPanel myPanel;
-  private final SequentialTaskExecutor myViewUpdater =
-      new SequentialTaskExecutor(PooledThreadExecutor.INSTANCE);
+  private final ExecutorService myViewUpdater =
+      new BoundedTaskExecutor(PooledThreadExecutor.INSTANCE, 1);
   private final Icon myActiveIcon = AllIcons.Toolwindows.Problems;
   private final Icon myPassiveIcon = IconLoader.getDisabledIcon(myActiveIcon);
 
diff --git a/clwb/src/com/google/idea/blaze/clwb/run/BlazeCidrDebuggableConfigurationFactory.java b/clwb/src/com/google/idea/blaze/clwb/run/BlazeCidrDebuggableConfigurationFactory.java
index 5363e69..5e740f3 100644
--- a/clwb/src/com/google/idea/blaze/clwb/run/BlazeCidrDebuggableConfigurationFactory.java
+++ b/clwb/src/com/google/idea/blaze/clwb/run/BlazeCidrDebuggableConfigurationFactory.java
@@ -15,9 +15,7 @@
  */
 package com.google.idea.blaze.clwb.run;
 
-import com.google.common.collect.ImmutableList;
 import com.google.idea.blaze.base.command.BlazeCommandName;
-import com.google.idea.blaze.base.command.BlazeFlags;
 import com.google.idea.blaze.base.ideinfo.TargetIdeInfo;
 import com.google.idea.blaze.base.ideinfo.TargetKey;
 import com.google.idea.blaze.base.model.BlazeProjectData;
@@ -58,7 +56,6 @@
     if (state != null) {
       if (kind != null && Kind.isTestRule(kind.toString())) {
         state.setCommand(BlazeCommandName.TEST);
-        state.setBlazeFlags(ImmutableList.of(BlazeFlags.TEST_OUTPUT_STREAMED));
       } else {
         state.setCommand(BlazeCommandName.RUN);
       }
diff --git a/clwb/src/com/google/idea/blaze/clwb/run/BlazeCidrLauncher.java b/clwb/src/com/google/idea/blaze/clwb/run/BlazeCidrLauncher.java
index 410cf57..f5ca00e 100644
--- a/clwb/src/com/google/idea/blaze/clwb/run/BlazeCidrLauncher.java
+++ b/clwb/src/com/google/idea/blaze/clwb/run/BlazeCidrLauncher.java
@@ -26,14 +26,18 @@
 import com.google.idea.blaze.base.projectview.ProjectViewManager;
 import com.google.idea.blaze.base.projectview.ProjectViewSet;
 import com.google.idea.blaze.base.run.BlazeCommandRunConfiguration;
+import com.google.idea.blaze.base.run.DistributedExecutorSupport;
 import com.google.idea.blaze.base.run.processhandler.LineProcessingProcessAdapter;
 import com.google.idea.blaze.base.run.processhandler.ScopedBlazeProcessHandler;
+import com.google.idea.blaze.base.run.smrunner.BlazeTestEventsHandler;
+import com.google.idea.blaze.base.run.smrunner.SmRunnerUtils;
 import com.google.idea.blaze.base.run.state.BlazeCommandRunConfigurationCommonState;
 import com.google.idea.blaze.base.scope.BlazeContext;
 import com.google.idea.blaze.base.scope.scopes.IssuesScope;
 import com.google.idea.blaze.base.settings.Blaze;
 import com.google.idea.blaze.base.settings.BlazeImportSettings;
 import com.google.idea.blaze.base.settings.BlazeImportSettingsManager;
+import com.google.idea.sdkcompat.cidr.CidrConsoleBuilderAdapter;
 import com.intellij.execution.ExecutionException;
 import com.intellij.execution.configurations.CommandLineState;
 import com.intellij.execution.configurations.GeneralCommandLine;
@@ -50,10 +54,8 @@
 import com.jetbrains.cidr.execution.debugger.CidrDebugProcess;
 import com.jetbrains.cidr.execution.debugger.CidrLocalDebugProcess;
 import com.jetbrains.cidr.execution.testing.CidrLauncher;
-import com.jetbrains.cidr.execution.testing.OCGoogleTestConsoleProperties;
 import java.io.File;
-import java.util.Objects;
-import org.jetbrains.annotations.NotNull;
+import javax.annotation.Nullable;
 
 /**
  * Handles running/debugging cc_test and cc_binary targets in CLion. Sets up gdb when debugging, and
@@ -68,7 +70,7 @@
   private final BlazeCidrRunConfigurationRunner runner;
   private final ExecutionEnvironment executionEnvironment;
 
-  public BlazeCidrLauncher(
+  BlazeCidrLauncher(
       BlazeCommandRunConfiguration configuration,
       BlazeCidrRunConfigurationRunner runner,
       ExecutionEnvironment environment) {
@@ -89,26 +91,39 @@
     ProjectViewSet projectViewSet = ProjectViewManager.getInstance(project).getProjectViewSet();
     LOG.assertTrue(projectViewSet != null);
 
-    state.setConsoleBuilder(createConsoleBuilder());
+    ImmutableList<String> testHandlerFlags = ImmutableList.of();
+    BlazeTestEventsHandler testEventsHandler =
+        useTestUi()
+            ? BlazeTestEventsHandler.getHandlerForTarget(project, configuration.getTarget())
+            : null;
+    if (testEventsHandler != null) {
+      testHandlerFlags = BlazeTestEventsHandler.getBlazeFlags(project);
+    }
 
-    BlazeCommand blazeCommand =
-        BlazeCommand.builder(Blaze.getBuildSystem(project), handlerState.getCommand())
+    BlazeCommand.Builder command =
+        BlazeCommand.builder(
+                Blaze.getBuildSystemProvider(project).getBinaryPath(), handlerState.getCommand())
             .addTargets(configuration.getTarget())
             .addBlazeFlags(BlazeFlags.buildFlags(project, ProjectViewSet.builder().build()))
+            .addBlazeFlags(testHandlerFlags)
             .addBlazeFlags(handlerState.getBlazeFlags())
-            .addExeFlags(handlerState.getExeFlags())
-            .build();
+            .addExeFlags(handlerState.getExeFlags());
+
+    command.addBlazeFlags(
+        DistributedExecutorSupport.getBlazeFlags(
+            project, handlerState.getRunOnDistributedExecutor()));
+
+    state.setConsoleBuilder(createConsoleBuilder(testEventsHandler));
 
     WorkspaceRoot workspaceRoot = WorkspaceRoot.fromImportSettings(importSettings);
     return new ScopedBlazeProcessHandler(
         project,
-        blazeCommand,
+        command.build(),
         workspaceRoot,
         new ScopedBlazeProcessHandler.ScopedProcessHandlerDelegate() {
           @Override
           public void onBlazeContextStart(BlazeContext context) {
-            context
-                .push(new IssuesScope(project));
+            context.push(new IssuesScope(project));
           }
 
           @Override
@@ -145,24 +160,21 @@
     CLionRunParameters parameters =
         new CLionRunParameters(
             new BlazeGDBDriverConfiguration(project, startupCommands, workspaceRoot), installer);
-    CidrDebugProcess result =
-        new CidrLocalDebugProcess(parameters, session, state.getConsoleBuilder());
 
-    return result;
+    return new CidrLocalDebugProcess(parameters, session, state.getConsoleBuilder());
   }
 
-  @NotNull
   @Override
   protected Project getProject() {
     return project;
   }
 
-  private CidrConsoleBuilder createConsoleBuilder() {
-    if (Objects.equals(handlerState.getCommand(), BlazeCommandName.TEST)) {
-      // Use the Google Test failure/success console instead of a standard console.
-      return new GoogleTestConsoleBuilder(configuration.getProject());
+  private CidrConsoleBuilder createConsoleBuilder(
+      @Nullable BlazeTestEventsHandler testEventsHandler) {
+    if (testEventsHandler != null) {
+      return new GoogleTestConsoleBuilder(configuration.getProject(), testEventsHandler);
     }
-    return new CidrConsoleBuilder(configuration.getProject());
+    return new CidrConsoleBuilderAdapter(configuration.getProject());
   }
 
   private ImmutableList<String> getGdbStartupCommands(File workingDir) {
@@ -176,20 +188,27 @@
     return ImmutableList.of(subPathCommand);
   }
 
-  private final class GoogleTestConsoleBuilder extends CidrConsoleBuilder {
-    private GoogleTestConsoleBuilder(Project project) {
+  private boolean useTestUi() {
+    return BlazeCommandName.TEST.equals(handlerState.getCommand())
+        && !handlerState.getRunOnDistributedExecutor();
+  }
+
+  private final class GoogleTestConsoleBuilder extends CidrConsoleBuilderAdapter {
+    private final BlazeTestEventsHandler testEventsHandler;
+
+    private GoogleTestConsoleBuilder(Project project, BlazeTestEventsHandler testEventsHandler) {
       super(project);
+      this.testEventsHandler = testEventsHandler;
       addFilter(new BlazeCidrTestOutputFilter(project));
     }
 
     @Override
     protected ConsoleView createConsole() {
-      OCGoogleTestConsoleProperties consoleProperties =
-          new OCGoogleTestConsoleProperties(
-              configuration,
-              executionEnvironment.getExecutor(),
-              executionEnvironment.getExecutionTarget());
-      return createConsole(configuration.getType(), consoleProperties);
+      return SmRunnerUtils.getConsoleView(
+          configuration.getProject(),
+          configuration,
+          executionEnvironment.getExecutor(),
+          testEventsHandler);
     }
   }
 }
diff --git a/clwb/src/com/google/idea/blaze/clwb/run/BlazeCidrRunConfigurationRunner.java b/clwb/src/com/google/idea/blaze/clwb/run/BlazeCidrRunConfigurationRunner.java
index 208cf11..c1b1f8d 100644
--- a/clwb/src/com/google/idea/blaze/clwb/run/BlazeCidrRunConfigurationRunner.java
+++ b/clwb/src/com/google/idea/blaze/clwb/run/BlazeCidrRunConfigurationRunner.java
@@ -123,7 +123,9 @@
                 context.output(new StatusOutput("Building debug binary"));
 
                 BlazeCommand.Builder command =
-                    BlazeCommand.builder(Blaze.getBuildSystem(project), BlazeCommandName.BUILD)
+                    BlazeCommand.builder(
+                            Blaze.getBuildSystemProvider(project).getBinaryPath(),
+                            BlazeCommandName.BUILD)
                         .addTargets(configuration.getTarget())
                         .addBlazeFlags(BlazeFlags.buildFlags(project, projectViewSet))
                         .addBlazeFlags(handlerState.getBlazeFlags());
diff --git a/clwb/src/com/google/idea/blaze/clwb/run/BlazeCidrTestOutputFilter.java b/clwb/src/com/google/idea/blaze/clwb/run/BlazeCidrTestOutputFilter.java
index b2498f1..cdd8900 100644
--- a/clwb/src/com/google/idea/blaze/clwb/run/BlazeCidrTestOutputFilter.java
+++ b/clwb/src/com/google/idea/blaze/clwb/run/BlazeCidrTestOutputFilter.java
@@ -22,7 +22,7 @@
 import com.intellij.execution.filters.RegexpFilter;
 import com.intellij.openapi.project.Project;
 import java.io.File;
-import org.jetbrains.annotations.Nullable;
+import javax.annotation.Nullable;
 
 /** Identifies file paths in CLion test output which aren't found by CidrPathConsoleFilter. */
 public class BlazeCidrTestOutputFilter extends RegexpFilter {
diff --git a/clwb/src/com/google/idea/blaze/clwb/run/BlazeCommandFlags.java b/clwb/src/com/google/idea/blaze/clwb/run/BlazeCommandFlags.java
deleted file mode 100644
index 12020f2..0000000
--- a/clwb/src/com/google/idea/blaze/clwb/run/BlazeCommandFlags.java
+++ /dev/null
@@ -1,132 +0,0 @@
-/*
- * Copyright 2016 The Bazel Authors. All rights reserved.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.google.idea.blaze.clwb.run;
-
-import com.google.common.base.Objects;
-import com.google.common.base.Strings;
-import com.google.common.collect.ImmutableList;
-import com.google.idea.blaze.base.ui.UiUtil;
-import com.intellij.openapi.util.InvalidDataException;
-import com.intellij.openapi.util.JDOMExternalizable;
-import com.intellij.openapi.util.WriteExternalException;
-import com.intellij.util.execution.ParametersListUtil;
-import java.util.List;
-import javax.swing.JComponent;
-import javax.swing.JLabel;
-import javax.swing.JTextArea;
-import org.jdom.Element;
-
-final class BlazeCommandFlags implements JDOMExternalizable {
-  public static final class Editor {
-    private final JTextArea blazeFlagsField = new JTextArea(5, 0);
-    private final JTextArea exeFlagsField = new JTextArea(5, 0);
-
-    public JComponent getEditorComponent() {
-      return UiUtil.createBox(
-          new JLabel("Blaze flags:"),
-          blazeFlagsField,
-          new JLabel("Executable flags:"),
-          exeFlagsField);
-    }
-
-    public void setText(BlazeCommandFlags blazeCommandFlags) {
-      blazeFlagsField.setText(ParametersListUtil.join(blazeCommandFlags.getBlazeFlags()));
-      exeFlagsField.setText(ParametersListUtil.join(blazeCommandFlags.getExeFlags()));
-    }
-
-    public BlazeCommandFlags getBlazeCommandFlags() {
-      ImmutableList<String> blazeFlags =
-          ImmutableList.copyOf(
-              ParametersListUtil.parse(Strings.nullToEmpty(blazeFlagsField.getText())));
-      ImmutableList<String> exeFlags =
-          ImmutableList.copyOf(
-              ParametersListUtil.parse(Strings.nullToEmpty(exeFlagsField.getText())));
-      return new BlazeCommandFlags(blazeFlags, exeFlags);
-    }
-  }
-
-  private static final String USER_BLAZE_FLAG_TAG = "blaze-user-flag";
-  private static final String USER_EXE_FLAG_TAG = "blaze-user-exe-flag";
-
-  private ImmutableList<String> blazeFlags = ImmutableList.of();
-  private ImmutableList<String> exeFlags = ImmutableList.of();
-
-  public BlazeCommandFlags() {
-    this.blazeFlags = ImmutableList.of();
-    this.exeFlags = ImmutableList.of();
-  }
-
-  public BlazeCommandFlags(ImmutableList<String> blazeFlags, ImmutableList<String> exeFlags) {
-    this.blazeFlags = blazeFlags;
-    this.exeFlags = exeFlags;
-  }
-
-  public ImmutableList<String> getBlazeFlags() {
-    return blazeFlags;
-  }
-
-  public ImmutableList<String> getExeFlags() {
-    return exeFlags;
-  }
-
-  @Override
-  public void readExternal(Element element) throws InvalidDataException {
-    blazeFlags = loadUserFlags(element, USER_BLAZE_FLAG_TAG);
-    exeFlags = loadUserFlags(element, USER_EXE_FLAG_TAG);
-  }
-
-  private static ImmutableList<String> loadUserFlags(Element root, String tag) {
-    ImmutableList.Builder<String> flagsBuilder = ImmutableList.builder();
-    for (Element e : root.getChildren(tag)) {
-      String flag = e.getTextTrim();
-      if (flag != null && !flag.isEmpty()) {
-        flagsBuilder.add(flag);
-      }
-    }
-    return flagsBuilder.build();
-  }
-
-  @Override
-  public void writeExternal(Element element) throws WriteExternalException {
-    saveUserFlags(element, blazeFlags, USER_BLAZE_FLAG_TAG);
-    saveUserFlags(element, exeFlags, USER_EXE_FLAG_TAG);
-  }
-
-  private static void saveUserFlags(Element root, List<String> flags, String tag) {
-    for (String flag : flags) {
-      Element child = new Element(tag);
-      child.setText(flag);
-      root.addContent(child);
-    }
-  }
-
-  @Override
-  public boolean equals(Object o) {
-    if (this == o) {
-      return true;
-    }
-    if (o == null || getClass() != o.getClass()) {
-      return false;
-    }
-    BlazeCommandFlags that = (BlazeCommandFlags) o;
-    return Objects.equal(blazeFlags, that.blazeFlags) && Objects.equal(exeFlags, that.exeFlags);
-  }
-
-  @Override
-  public int hashCode() {
-    return Objects.hashCode(blazeFlags, exeFlags);
-  }
-}
diff --git a/clwb/src/com/google/idea/blaze/clwb/run/producers/BlazeCidrTestConfigurationProducer.java b/clwb/src/com/google/idea/blaze/clwb/run/producers/BlazeCidrTestConfigurationProducer.java
index 3589c9a..0a48b17 100644
--- a/clwb/src/com/google/idea/blaze/clwb/run/producers/BlazeCidrTestConfigurationProducer.java
+++ b/clwb/src/com/google/idea/blaze/clwb/run/producers/BlazeCidrTestConfigurationProducer.java
@@ -17,33 +17,17 @@
 
 import com.google.common.collect.ImmutableList;
 import com.google.idea.blaze.base.command.BlazeCommandName;
-import com.google.idea.blaze.base.command.BlazeFlags;
-import com.google.idea.blaze.base.model.primitives.Label;
 import com.google.idea.blaze.base.run.BlazeCommandRunConfiguration;
 import com.google.idea.blaze.base.run.BlazeCommandRunConfigurationType;
-import com.google.idea.blaze.base.run.TestTargetHeuristic;
 import com.google.idea.blaze.base.run.producers.BlazeRunConfigurationProducer;
 import com.google.idea.blaze.base.run.state.BlazeCommandRunConfigurationCommonState;
 import com.google.idea.blaze.base.settings.Blaze;
+import com.google.idea.blaze.clwb.run.test.BlazeCidrTestTarget;
 import com.intellij.execution.Location;
 import com.intellij.execution.actions.ConfigurationContext;
 import com.intellij.openapi.actionSystem.LangDataKeys;
-import com.intellij.openapi.util.Couple;
 import com.intellij.openapi.util.Ref;
 import com.intellij.psi.PsiElement;
-import com.intellij.psi.util.PsiTreeUtil;
-import com.jetbrains.cidr.execution.testing.CidrTestUtil;
-import com.jetbrains.cidr.lang.psi.OCFile;
-import com.jetbrains.cidr.lang.psi.OCFunctionDefinition;
-import com.jetbrains.cidr.lang.psi.OCMacroCall;
-import com.jetbrains.cidr.lang.psi.OCMacroCallArgument;
-import com.jetbrains.cidr.lang.psi.OCStruct;
-import com.jetbrains.cidr.lang.symbols.OCSymbol;
-import com.jetbrains.cidr.lang.symbols.cpp.OCFunctionSymbol;
-import com.jetbrains.cidr.lang.symbols.cpp.OCStructSymbol;
-import com.jetbrains.cidr.lang.symbols.cpp.OCSymbolWithQualifiedName;
-import java.util.Collection;
-import java.util.List;
 import java.util.Objects;
 import javax.annotation.Nullable;
 
@@ -51,52 +35,6 @@
 public class BlazeCidrTestConfigurationProducer
     extends BlazeRunConfigurationProducer<BlazeCommandRunConfiguration> {
 
-  private static class TestTarget {
-    @Nullable
-    private static TestTarget createFromFile(@Nullable PsiElement element) {
-      return createFromClassAndMethod(element, null, null);
-    }
-
-    @Nullable
-    private static TestTarget createFromClass(@Nullable PsiElement element, String className) {
-      return createFromClassAndMethod(element, className, null);
-    }
-
-    @Nullable
-    private static TestTarget createFromClassAndMethod(
-        @Nullable PsiElement element, String classOrSuiteName, @Nullable String testName) {
-      Label label = TestTargetHeuristic.testTargetForPsiElement(element);
-      if (label == null) {
-        return null;
-      }
-      String filter = null;
-      if (classOrSuiteName != null) {
-        filter = classOrSuiteName;
-        if (testName != null) {
-          filter += "." + testName;
-        }
-      }
-      return new TestTarget(element, label, filter);
-    }
-
-    private final PsiElement element;
-    private final Label label;
-    @Nullable private final String testFilterArg;
-    private final String name;
-
-    private TestTarget(PsiElement element, Label label, @Nullable String testFilter) {
-      this.element = element;
-      this.label = label;
-      if (testFilter != null) {
-        testFilterArg = BlazeFlags.TEST_FILTER + "=" + testFilter;
-        name = String.format("%s (%s)", testFilter, label.toString());
-      } else {
-        testFilterArg = null;
-        name = label.toString();
-      }
-    }
-  }
-
   public BlazeCidrTestConfigurationProducer() {
     super(BlazeCommandRunConfigurationType.getInstance());
   }
@@ -122,7 +60,7 @@
     if (element == null) {
       return false;
     }
-    TestTarget testObject = findTestObject(element);
+    BlazeCidrTestTarget testObject = BlazeCidrTestTarget.findTestObject(element);
     if (testObject == null) {
       return false;
     }
@@ -136,10 +74,10 @@
     handlerState.setCommand(BlazeCommandName.TEST);
 
     ImmutableList.Builder<String> flags = ImmutableList.builder();
-    if (testObject.testFilterArg != null) {
-      flags.add(testObject.testFilterArg);
+    String testFilter = testObject.getTestFilterFlag();
+    if (testFilter != null) {
+      flags.add(testFilter);
     }
-    flags.add(BlazeFlags.TEST_OUTPUT_STREAMED);
     flags.addAll(handlerState.getBlazeFlags());
 
     handlerState.setBlazeFlags(flags.build());
@@ -164,100 +102,11 @@
     if (element == null) {
       return false;
     }
-    TestTarget testObject = findTestObject(element);
+    BlazeCidrTestTarget testObject = BlazeCidrTestTarget.findTestObject(element);
     if (testObject == null) {
       return false;
     }
-    List<String> flags = handlerState.getBlazeFlags();
     return testObject.label.equals(configuration.getTarget())
-        && (testObject.testFilterArg == null || flags.contains(testObject.testFilterArg));
-  }
-
-  @Nullable
-  private static TestTarget findTestObject(PsiElement element) {
-    // Copied from on CidrGoogleTestRunConfigurationProducer::findTestObject.
-    // Precedence order (decreasing): class/function, macro, file
-    PsiElement parent =
-        PsiTreeUtil.getNonStrictParentOfType(element, OCFunctionDefinition.class, OCStruct.class);
-
-    OCStructSymbol parentSymbol;
-    if (parent instanceof OCStruct
-        && ((parentSymbol = ((OCStruct) parent).getSymbol()) != null)
-        && CidrTestUtil.isGoogleTestClass(parentSymbol)) {
-      Couple<String> name = CidrTestUtil.extractGoogleTestName(parentSymbol);
-      if (name != null) {
-        return TestTarget.createFromClassAndMethod(parent, name.first, name.second);
-      }
-      String className = parentSymbol.getQualifiedName().getName();
-      return TestTarget.createFromClass(parent, className);
-    } else if (parent instanceof OCFunctionDefinition) {
-      OCFunctionSymbol symbol = ((OCFunctionDefinition) parent).getSymbol();
-      if (symbol != null) {
-        OCSymbolWithQualifiedName<?> resolvedOwner = symbol.getResolvedOwner();
-        if (resolvedOwner != null) {
-          OCSymbol<?> owner = resolvedOwner.getDefinitionSymbol();
-          if (owner instanceof OCStructSymbol
-              && CidrTestUtil.isGoogleTestClass((OCStructSymbol) owner)) {
-            OCStruct struct = (OCStruct) owner.locateDefinition();
-            Couple<String> name = CidrTestUtil.extractGoogleTestName((OCStructSymbol) owner);
-            if (name != null) {
-              return TestTarget.createFromClassAndMethod(struct, name.first, name.second);
-            }
-            return TestTarget.createFromClass(
-                struct, ((OCStructSymbol) owner).getQualifiedName().getName());
-          }
-        }
-      }
-    }
-
-    // if we're still here, let's test for a macro and, as a last resort, a file.
-    parent = PsiTreeUtil.getNonStrictParentOfType(element, OCMacroCall.class, OCFile.class);
-    if (parent instanceof OCMacroCall) {
-      OCMacroCall gtestMacro = CidrTestUtil.findGoogleTestMacros(parent);
-      if (gtestMacro != null) {
-        List<OCMacroCallArgument> arguments = gtestMacro.getArguments();
-        if (arguments.size() >= 2) {
-          OCMacroCallArgument suiteArg = arguments.get(0);
-          OCMacroCallArgument testArg = arguments.get(1);
-
-          // if the element is the first argument of macro call,
-          // then running entire suite, otherwise only a current test
-          boolean isSuite =
-              isFirstArgument(PsiTreeUtil.getParentOfType(element, OCMacroCallArgument.class))
-                  || isFirstArgument(element.getPrevSibling());
-          String suiteName = CidrTestUtil.extractArgumentValue(suiteArg);
-          String testName = CidrTestUtil.extractArgumentValue(testArg);
-          OCStructSymbol symbol =
-              CidrTestUtil.findGoogleTestSymbol(element.getProject(), suiteName, testName);
-          if (symbol != null) {
-            OCStruct targetElement = (OCStruct) symbol.locateDefinition();
-            return TestTarget.createFromClassAndMethod(
-                targetElement, suiteName, isSuite ? null : testName);
-          }
-        }
-      }
-      Couple<String> suite = CidrTestUtil.extractFullSuiteNameFromMacro(parent);
-      if (suite != null) {
-        Collection<OCStructSymbol> res =
-            CidrTestUtil.findGoogleTestSymbolsForSuiteRandomly(
-                element.getProject(), suite.first, true);
-        if (res.size() != 0) {
-          OCStruct struct = (OCStruct) res.iterator().next().locateDefinition();
-          return TestTarget.createFromClassAndMethod(struct, suite.first, null);
-        }
-      }
-    } else if (parent instanceof OCFile) {
-      return TestTarget.createFromFile(parent);
-    }
-    return null;
-  }
-
-  private static boolean isFirstArgument(@Nullable PsiElement element) {
-    OCMacroCall macroCall = PsiTreeUtil.getParentOfType(element, OCMacroCall.class);
-    if (macroCall != null) {
-      List<OCMacroCallArgument> arguments = macroCall.getArguments();
-      return arguments.size() > 0 && arguments.get(0).equals(element);
-    }
-    return false;
+        && Objects.equals(handlerState.getTestFilterFlag(), testObject.getTestFilterFlag());
   }
 }
diff --git a/clwb/src/com/google/idea/blaze/clwb/run/test/BlazeCidrTestEventsHandler.java b/clwb/src/com/google/idea/blaze/clwb/run/test/BlazeCidrTestEventsHandler.java
new file mode 100644
index 0000000..831a81b
--- /dev/null
+++ b/clwb/src/com/google/idea/blaze/clwb/run/test/BlazeCidrTestEventsHandler.java
@@ -0,0 +1,78 @@
+/*
+ * Copyright 2017 The Bazel Authors. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.idea.blaze.clwb.run.test;
+
+import com.google.common.base.Joiner;
+import com.google.idea.blaze.base.command.BlazeFlags;
+import com.google.idea.blaze.base.model.primitives.Kind;
+import com.google.idea.blaze.base.run.smrunner.BlazeTestEventsHandler;
+import com.intellij.execution.Location;
+import com.intellij.execution.testframework.sm.runner.SMTestLocator;
+import com.intellij.openapi.project.Project;
+import com.intellij.util.io.URLUtil;
+import com.jetbrains.cidr.execution.testing.OCGoogleTestLocationProvider;
+import java.util.ArrayList;
+import java.util.EnumSet;
+import java.util.List;
+import javax.annotation.Nullable;
+
+/** Provides C/C++ specific methods needed by the SM-runner test UI. */
+public class BlazeCidrTestEventsHandler extends BlazeTestEventsHandler {
+
+  @Override
+  protected EnumSet<Kind> handledKinds() {
+    return EnumSet.of(Kind.CC_TEST);
+  }
+
+  @Override
+  public SMTestLocator getTestLocator() {
+    return OCGoogleTestLocationProvider.INSTANCE;
+  }
+
+  @Nullable
+  @Override
+  public String getTestFilter(Project project, List<Location<?>> testLocations) {
+    List<String> filters = new ArrayList<>();
+    for (Location<?> location : testLocations) {
+      BlazeCidrTestTarget target = BlazeCidrTestTarget.findTestObject(location.getPsiElement());
+      if (target != null && target.testFilter != null) {
+        filters.add(target.testFilter);
+      }
+    }
+    if (filters.isEmpty()) {
+      return null;
+    }
+    return String.format("%s=%s", BlazeFlags.TEST_FILTER, Joiner.on(':').join(filters));
+  }
+
+  @Override
+  public String suiteLocationUrl(@Nullable Kind kind, String name) {
+    return OCGoogleTestLocationProvider.PROTOCOL + URLUtil.SCHEME_SEPARATOR + name;
+  }
+
+  @Override
+  public String testLocationUrl(
+      @Nullable Kind kind, String parentSuite, String name, @Nullable String className) {
+    if (className == null) {
+      return OCGoogleTestLocationProvider.PROTOCOL + URLUtil.SCHEME_SEPARATOR + name;
+    }
+    return OCGoogleTestLocationProvider.PROTOCOL
+        + URLUtil.SCHEME_SEPARATOR
+        + className
+        + "."
+        + name;
+  }
+}
diff --git a/clwb/src/com/google/idea/blaze/clwb/run/test/BlazeCidrTestTarget.java b/clwb/src/com/google/idea/blaze/clwb/run/test/BlazeCidrTestTarget.java
new file mode 100644
index 0000000..acf3966
--- /dev/null
+++ b/clwb/src/com/google/idea/blaze/clwb/run/test/BlazeCidrTestTarget.java
@@ -0,0 +1,178 @@
+/*
+ * Copyright 2017 The Bazel Authors. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.idea.blaze.clwb.run.test;
+
+import com.google.idea.blaze.base.command.BlazeFlags;
+import com.google.idea.blaze.base.model.primitives.Label;
+import com.google.idea.blaze.base.run.TestTargetHeuristic;
+import com.intellij.openapi.util.Couple;
+import com.intellij.psi.PsiElement;
+import com.intellij.psi.util.PsiTreeUtil;
+import com.jetbrains.cidr.execution.testing.CidrTestUtil;
+import com.jetbrains.cidr.lang.psi.OCFile;
+import com.jetbrains.cidr.lang.psi.OCFunctionDefinition;
+import com.jetbrains.cidr.lang.psi.OCMacroCall;
+import com.jetbrains.cidr.lang.psi.OCMacroCallArgument;
+import com.jetbrains.cidr.lang.psi.OCStruct;
+import com.jetbrains.cidr.lang.symbols.OCSymbol;
+import com.jetbrains.cidr.lang.symbols.cpp.OCFunctionSymbol;
+import com.jetbrains.cidr.lang.symbols.cpp.OCStructSymbol;
+import com.jetbrains.cidr.lang.symbols.cpp.OCSymbolWithQualifiedName;
+import java.util.Collection;
+import java.util.List;
+import javax.annotation.Nullable;
+
+/** A blaze cpp test target, together with optional test filter. */
+public class BlazeCidrTestTarget {
+
+  public final PsiElement element;
+  public final Label label;
+  @Nullable public final String testFilter;
+  public final String name;
+
+  private BlazeCidrTestTarget(PsiElement element, Label label, @Nullable String testFilter) {
+    this.element = element;
+    this.label = label;
+    this.testFilter = testFilter;
+    if (testFilter != null) {
+      name = String.format("%s (%s)", testFilter, label.toString());
+    } else {
+      name = label.toString();
+    }
+  }
+
+  /** The raw test filter string with '--test_filter=' prepended, or null if there is no filter. */
+  @Nullable
+  public String getTestFilterFlag() {
+    return testFilter != null ? BlazeFlags.TEST_FILTER + "=" + testFilter : null;
+  }
+
+  @Nullable
+  private static BlazeCidrTestTarget createFromFile(@Nullable PsiElement element) {
+    return createFromClassAndMethod(element, null, null);
+  }
+
+  @Nullable
+  private static BlazeCidrTestTarget createFromClass(
+      @Nullable PsiElement element, String className) {
+    return createFromClassAndMethod(element, className, null);
+  }
+
+  @Nullable
+  private static BlazeCidrTestTarget createFromClassAndMethod(
+      @Nullable PsiElement element, String classOrSuiteName, @Nullable String testName) {
+    Label label = TestTargetHeuristic.testTargetForPsiElement(element);
+    if (label == null) {
+      return null;
+    }
+    String filter = null;
+    if (classOrSuiteName != null) {
+      filter = classOrSuiteName;
+      if (testName != null) {
+        filter += "." + testName;
+      }
+    }
+    return new BlazeCidrTestTarget(element, label, filter);
+  }
+
+  @Nullable
+  public static BlazeCidrTestTarget findTestObject(PsiElement element) {
+    // Copied from on CidrGoogleTestRunConfigurationProducer::findTestObject.
+    // Precedence order (decreasing): class/function, macro, file
+    PsiElement parent =
+        PsiTreeUtil.getNonStrictParentOfType(element, OCFunctionDefinition.class, OCStruct.class);
+
+    OCStructSymbol parentSymbol;
+    if (parent instanceof OCStruct
+        && ((parentSymbol = ((OCStruct) parent).getSymbol()) != null)
+        && CidrTestUtil.isGoogleTestClass(parentSymbol)) {
+      Couple<String> name = CidrTestUtil.extractGoogleTestName(parentSymbol);
+      if (name != null) {
+        return createFromClassAndMethod(parent, name.first, name.second);
+      }
+      String className = parentSymbol.getQualifiedName().getName();
+      return createFromClass(parent, className);
+    } else if (parent instanceof OCFunctionDefinition) {
+      OCFunctionSymbol symbol = ((OCFunctionDefinition) parent).getSymbol();
+      if (symbol != null) {
+        OCSymbolWithQualifiedName<?> resolvedOwner = symbol.getResolvedOwner();
+        if (resolvedOwner != null) {
+          OCSymbol<?> owner = resolvedOwner.getDefinitionSymbol();
+          if (owner instanceof OCStructSymbol
+              && CidrTestUtil.isGoogleTestClass((OCStructSymbol) owner)) {
+            OCStruct struct = (OCStruct) owner.locateDefinition();
+            Couple<String> name = CidrTestUtil.extractGoogleTestName((OCStructSymbol) owner);
+            if (name != null) {
+              return createFromClassAndMethod(struct, name.first, name.second);
+            }
+            return createFromClass(
+                struct, ((OCStructSymbol) owner).getQualifiedName().getName());
+          }
+        }
+      }
+    }
+
+    // if we're still here, let's test for a macro and, as a last resort, a file.
+    parent = PsiTreeUtil.getNonStrictParentOfType(element, OCMacroCall.class, OCFile.class);
+    if (parent instanceof OCMacroCall) {
+      OCMacroCall gtestMacro = CidrTestUtil.findGoogleTestMacros(parent);
+      if (gtestMacro != null) {
+        List<OCMacroCallArgument> arguments = gtestMacro.getArguments();
+        if (arguments.size() >= 2) {
+          OCMacroCallArgument suiteArg = arguments.get(0);
+          OCMacroCallArgument testArg = arguments.get(1);
+
+          // if the element is the first argument of macro call,
+          // then running entire suite, otherwise only a current test
+          boolean isSuite =
+              isFirstArgument(PsiTreeUtil.getParentOfType(element, OCMacroCallArgument.class))
+                  || isFirstArgument(element.getPrevSibling());
+          String suiteName = CidrTestUtil.extractArgumentValue(suiteArg);
+          String testName = CidrTestUtil.extractArgumentValue(testArg);
+          OCStructSymbol symbol =
+              CidrTestUtil.findGoogleTestSymbol(element.getProject(), suiteName, testName);
+          if (symbol != null) {
+            OCStruct targetElement = (OCStruct) symbol.locateDefinition();
+            return createFromClassAndMethod(
+                targetElement, suiteName, isSuite ? null : testName);
+          }
+        }
+      }
+      Couple<String> suite = CidrTestUtil.extractFullSuiteNameFromMacro(parent);
+      if (suite != null) {
+        Collection<OCStructSymbol> res =
+            CidrTestUtil.findGoogleTestSymbolsForSuiteRandomly(
+                element.getProject(), suite.first, true);
+        if (res.size() != 0) {
+          OCStruct struct = (OCStruct) res.iterator().next().locateDefinition();
+          return createFromClassAndMethod(struct, suite.first, null);
+        }
+      }
+    } else if (parent instanceof OCFile) {
+      return createFromFile(parent);
+    }
+    return null;
+  }
+
+  private static boolean isFirstArgument(@Nullable PsiElement element) {
+    OCMacroCall macroCall = PsiTreeUtil.getParentOfType(element, OCMacroCall.class);
+    if (macroCall != null) {
+      List<OCMacroCallArgument> arguments = macroCall.getArguments();
+      return arguments.size() > 0 && arguments.get(0).equals(element);
+    }
+    return false;
+  }
+}
diff --git a/clwb/src/com/google/idea/blaze/plugin/ClwbSpecificInitializer.java b/clwb/src/com/google/idea/blaze/plugin/ClwbSpecificInitializer.java
index 4a3445e..086bbbf 100644
--- a/clwb/src/com/google/idea/blaze/plugin/ClwbSpecificInitializer.java
+++ b/clwb/src/com/google/idea/blaze/plugin/ClwbSpecificInitializer.java
@@ -16,12 +16,8 @@
 package com.google.idea.blaze.plugin;
 
 import com.google.idea.blaze.base.plugin.BlazeActionRemover;
+import com.google.idea.sdkcompat.clion.CMakeActionList;
 import com.intellij.openapi.components.ApplicationComponent;
-import com.jetbrains.cidr.cpp.cmake.actions.ChangeCMakeProjectContentRootAction;
-import com.jetbrains.cidr.cpp.cmake.actions.DropCMakeCacheAction;
-import com.jetbrains.cidr.cpp.cmake.actions.OpenCMakeSettingsAction;
-import com.jetbrains.cidr.cpp.cmake.actions.ReloadCMakeProjectAction;
-import com.jetbrains.cidr.cpp.cmake.actions.ToggleCMakeAutoReloadAction;
 
 /** Runs on startup. */
 public class ClwbSpecificInitializer extends ApplicationComponent.Adapter {
@@ -33,12 +29,8 @@
 
   // The original actions will be visible only on plain IDEA projects.
   private static void hideCMakeActions() {
-    BlazeActionRemover.hideAction(ChangeCMakeProjectContentRootAction.ID);
-    BlazeActionRemover.hideAction(DropCMakeCacheAction.ID);
-    BlazeActionRemover.hideAction(OpenCMakeSettingsAction.ID);
-    BlazeActionRemover.hideAction(ReloadCMakeProjectAction.ID);
-    BlazeActionRemover.hideAction(ToggleCMakeAutoReloadAction.ID);
-    // 'CMake' > 'Show Generated CMake Files' action
-    BlazeActionRemover.hideAction("CMake.ShowGeneratedDir");
+    for (String actionId : CMakeActionList.CMAKE_ACTION_IDS) {
+      BlazeActionRemover.hideAction(actionId);
+    }
   }
 }
diff --git a/common/actionhelper/src/com/google/idea/common/actionhelper/ActionPresentationHelper.java b/common/actionhelper/src/com/google/idea/common/actionhelper/ActionPresentationHelper.java
index 69c5e1e..c90dd03 100644
--- a/common/actionhelper/src/com/google/idea/common/actionhelper/ActionPresentationHelper.java
+++ b/common/actionhelper/src/com/google/idea/common/actionhelper/ActionPresentationHelper.java
@@ -15,6 +15,7 @@
  */
 package com.google.idea.common.actionhelper;
 
+import com.intellij.openapi.actionSystem.ActionPlaces;
 import com.intellij.openapi.actionSystem.AnActionEvent;
 import com.intellij.openapi.actionSystem.Presentation;
 import com.intellij.openapi.vfs.VirtualFile;
@@ -24,13 +25,14 @@
 /** Helps setting the presentation enabled/visible/text states. */
 public class ActionPresentationHelper {
 
-  private final Presentation presentation;
+  private final AnActionEvent event;
   private boolean enabled = true;
   private boolean visible = true;
   private boolean disableWithoutSubject;
   private boolean hasSubject;
   private String text;
   private String subjectText;
+  private boolean hideInContextMenuIfDisabled;
 
   /** Converts a subject to a string */
   @FunctionalInterface
@@ -38,12 +40,12 @@
     String subjectToString(T subject);
   }
 
-  private ActionPresentationHelper(Presentation presentation) {
-    this.presentation = presentation;
+  private ActionPresentationHelper(AnActionEvent event) {
+    this.event = event;
   }
 
-  public static ActionPresentationHelper of(AnActionEvent e) {
-    return new ActionPresentationHelper(e.getPresentation());
+  public static ActionPresentationHelper of(AnActionEvent event) {
+    return new ActionPresentationHelper(event);
   }
 
   /** Disables the action if the condition is true. */
@@ -58,6 +60,11 @@
     return this;
   }
 
+  public ActionPresentationHelper hideInContextMenuIfDisabled() {
+    this.hideInContextMenuIfDisabled = true;
+    return this;
+  }
+
   /** Disables the action if no subject has been provided. */
   public ActionPresentationHelper disableWithoutSubject() {
     this.disableWithoutSubject = true;
@@ -164,6 +171,12 @@
     if (disableWithoutSubject) {
       enabled = enabled && hasSubject;
     }
+    boolean visible = this.visible;
+    if (hideInContextMenuIfDisabled && !enabled && ActionPlaces.isPopupPlace(event.getPlace())) {
+      visible = false;
+    }
+
+    Presentation presentation = event.getPresentation();
     presentation.setEnabled(enabled);
     presentation.setVisible(visible);
 
diff --git a/common/experiments/src/com/google/idea/common/experiments/WebExperimentSyncer.java b/common/experiments/src/com/google/idea/common/experiments/WebExperimentSyncer.java
index 464a62d..43ab2b4 100644
--- a/common/experiments/src/com/google/idea/common/experiments/WebExperimentSyncer.java
+++ b/common/experiments/src/com/google/idea/common/experiments/WebExperimentSyncer.java
@@ -115,7 +115,7 @@
       logger.debug("About to fetch experiments.");
       return HttpRequests.request(
               System.getProperty(EXPERIMENTS_URL_PROPERTY, DEFAULT_EXPERIMENT_URL) + pluginName)
-          .readString(null /* progress indicator */);
+          .readString(/* progress indicator */ null);
     }
   }
 
@@ -144,10 +144,10 @@
         setExperimentValues(mapBuilder);
 
         logger.debug("Successfully fetched experiments: " + getExperimentValues());
-        scheduleNextRefresh(true /* refreshWasSuccessful */);
+        scheduleNextRefresh(/* refreshWasSuccessful */ true);
       } catch (InterruptedException | ExecutionException | RuntimeException e) {
         logger.debug("Error fetching experiments", e);
-        scheduleNextRefresh(false /* refreshWasSuccessful */);
+        scheduleNextRefresh(/* refreshWasSuccessful */ false);
       }
     }
   }
diff --git a/cpp/src/com/google/idea/blaze/cpp/BlazeCWorkspace.java b/cpp/src/com/google/idea/blaze/cpp/BlazeCWorkspace.java
index abcb294..c0f5b94 100644
--- a/cpp/src/com/google/idea/blaze/cpp/BlazeCWorkspace.java
+++ b/cpp/src/com/google/idea/blaze/cpp/BlazeCWorkspace.java
@@ -19,37 +19,26 @@
 import com.google.common.collect.ImmutableList;
 import com.google.idea.blaze.base.model.BlazeProjectData;
 import com.google.idea.blaze.base.scope.BlazeContext;
-import com.google.idea.blaze.base.settings.Blaze;
+import com.google.idea.sdkcompat.cidr.OCWorkspaceAdapter;
 import com.intellij.openapi.components.ServiceManager;
 import com.intellij.openapi.diagnostic.Logger;
 import com.intellij.openapi.project.Project;
 import com.intellij.openapi.vfs.VirtualFile;
 import com.jetbrains.cidr.lang.symbols.OCSymbol;
 import com.jetbrains.cidr.lang.workspace.OCResolveConfiguration;
-import com.jetbrains.cidr.lang.workspace.OCWorkspace;
-import com.jetbrains.cidr.lang.workspace.OCWorkspaceModificationTrackers;
 import java.util.Collection;
 import java.util.List;
 import javax.annotation.Nullable;
 
 /** Main entry point for C/CPP configuration data. */
-public final class BlazeCWorkspace implements OCWorkspace {
+public final class BlazeCWorkspace extends OCWorkspaceAdapter {
   private static final Logger logger = Logger.getInstance(BlazeCWorkspace.class);
 
-  @Nullable private final Project project;
-  @Nullable private final OCWorkspaceModificationTrackers modTrackers;
-
-  @Nullable private BlazeConfigurationResolver configurationResolver;
+  private final BlazeConfigurationResolver configurationResolver;
 
   private BlazeCWorkspace(Project project) {
-    if (Blaze.isBlazeProject(project)) {
-      this.project = project;
-      this.modTrackers = new OCWorkspaceModificationTrackers(project);
-      this.configurationResolver = new BlazeConfigurationResolver(project);
-    } else {
-      this.project = null;
-      this.modTrackers = null;
-    }
+    super(project);
+    this.configurationResolver = new BlazeConfigurationResolver(project);
   }
 
   public static BlazeCWorkspace getInstance(Project project) {
@@ -57,13 +46,8 @@
   }
 
   public void update(BlazeContext context, BlazeProjectData blazeProjectData) {
-    logger.assertTrue(project != null);
-    logger.assertTrue(modTrackers != null);
-    logger.assertTrue(configurationResolver != null);
-
-    long start = System.currentTimeMillis();
-
     // Non-incremental update to our c configurations.
+    long start = System.currentTimeMillis();
     configurationResolver.update(context, blazeProjectData);
     long end = System.currentTimeMillis();
 
@@ -97,29 +81,15 @@
     return false;
   }
 
-  @Nullable
-  @Override
-  public OCResolveConfiguration getSelectedResolveConfiguration() {
-    return null;
-  }
-
-  @Override
-  public OCWorkspaceModificationTrackers getModificationTrackers() {
-    logger.assertTrue(modTrackers != null);
-    return modTrackers;
-  }
-
   @Override
   public List<? extends OCResolveConfiguration> getConfigurations() {
-    return configurationResolver == null
-        ? ImmutableList.of()
-        : configurationResolver.getAllConfigurations();
+    return configurationResolver.getAllConfigurations();
   }
 
   @Override
   public List<? extends OCResolveConfiguration> getConfigurationsForFile(
       @Nullable VirtualFile sourceFile) {
-    if (sourceFile == null || !sourceFile.isValid() || configurationResolver == null) {
+    if (sourceFile == null || !sourceFile.isValid()) {
       return ImmutableList.of();
     }
     OCResolveConfiguration config = configurationResolver.getConfigurationForFile(sourceFile);
diff --git a/cpp/src/com/google/idea/blaze/cpp/BlazeConfigurationResolver.java b/cpp/src/com/google/idea/blaze/cpp/BlazeConfigurationResolver.java
index 5e6e8b9..f4b5cd6 100644
--- a/cpp/src/com/google/idea/blaze/cpp/BlazeConfigurationResolver.java
+++ b/cpp/src/com/google/idea/blaze/cpp/BlazeConfigurationResolver.java
@@ -34,11 +34,13 @@
 import com.google.idea.blaze.base.model.BlazeProjectData;
 import com.google.idea.blaze.base.model.primitives.ExecutionRootPath;
 import com.google.idea.blaze.base.model.primitives.LanguageClass;
+import com.google.idea.blaze.base.model.primitives.WorkspaceRoot;
 import com.google.idea.blaze.base.scope.BlazeContext;
 import com.google.idea.blaze.base.scope.Scope;
 import com.google.idea.blaze.base.scope.ScopedFunction;
 import com.google.idea.blaze.base.scope.output.IssueOutput;
 import com.google.idea.blaze.base.scope.scopes.TimingScope;
+import com.google.idea.blaze.base.settings.Blaze;
 import com.google.idea.blaze.base.sync.workspace.ExecutionRootPathResolver;
 import com.google.idea.blaze.base.sync.workspace.WorkspacePathResolver;
 import com.google.idea.blaze.base.targetmaps.SourceToTargetMap;
@@ -91,7 +93,7 @@
         buildBlazeConfigurationMap(context, blazeProjectData, toolchainLookupMap, headerRoots);
   }
 
-  private static ImmutableMap<File, VirtualFile> collectHeaderRoots(
+  private ImmutableMap<File, VirtualFile> collectHeaderRoots(
       BlazeContext parentContext,
       BlazeProjectData blazeProjectData,
       ImmutableMap<TargetKey, CToolchainIdeInfo> toolchainLookupMap) {
@@ -107,10 +109,14 @@
             });
   }
 
-  private static ImmutableMap<File, VirtualFile> doCollectHeaderRoots(
+  private ImmutableMap<File, VirtualFile> doCollectHeaderRoots(
       BlazeContext context, BlazeProjectData projectData, Set<ExecutionRootPath> rootPaths) {
     ExecutionRootPathResolver pathResolver =
-        new ExecutionRootPathResolver(projectData.blazeRoots, projectData.workspacePathResolver);
+        new ExecutionRootPathResolver(
+            Blaze.getBuildSystem(project),
+            WorkspaceRoot.fromProject(project),
+            projectData.blazeRoots.executionRoot,
+            projectData.workspacePathResolver);
     ConcurrentMap<File, VirtualFile> rootsMap = Maps.newConcurrentMap();
     List<ListenableFuture<Void>> futures = Lists.newArrayListWithCapacity(rootPaths.size());
     for (ExecutionRootPath path : rootPaths) {
@@ -253,7 +259,10 @@
             BlazeResolveConfiguration.createConfigurationForTarget(
                 project,
                 new ExecutionRootPathResolver(
-                    blazeProjectData.blazeRoots, blazeProjectData.workspacePathResolver),
+                    Blaze.getBuildSystem(project),
+                    WorkspaceRoot.fromProject(project),
+                    blazeProjectData.blazeRoots.executionRoot,
+                    blazeProjectData.workspacePathResolver),
                 blazeProjectData.workspacePathResolver,
                 headerRoots,
                 blazeProjectData.targetMap.get(targetKey),
diff --git a/cpp/src/com/google/idea/blaze/cpp/BlazeCppSymbolRebuildSyncListener.java b/cpp/src/com/google/idea/blaze/cpp/BlazeCppSymbolRebuildSyncListener.java
index 07b1743..fb9e190 100644
--- a/cpp/src/com/google/idea/blaze/cpp/BlazeCppSymbolRebuildSyncListener.java
+++ b/cpp/src/com/google/idea/blaze/cpp/BlazeCppSymbolRebuildSyncListener.java
@@ -21,12 +21,12 @@
 import com.google.idea.blaze.base.settings.BlazeImportSettings;
 import com.google.idea.blaze.base.sync.BlazeSyncParams.SyncMode;
 import com.google.idea.blaze.base.sync.SyncListener;
+import com.google.idea.sdkcompat.cidr.OCWorkspaceModificationTrackersCompatUtils;
 import com.google.idea.sdkcompat.transactions.Transactions;
 import com.intellij.openapi.application.ApplicationManager;
 import com.intellij.openapi.project.Project;
 import com.jetbrains.cidr.lang.workspace.OCWorkspace;
 import com.jetbrains.cidr.lang.workspace.OCWorkspaceManager;
-import com.jetbrains.cidr.lang.workspace.OCWorkspaceModificationTrackers;
 
 /** Runs after sync, triggering a rebuild of the symbol tables. */
 public class BlazeCppSymbolRebuildSyncListener extends SyncListener.Adapter {
@@ -45,20 +45,16 @@
     if (!(workspace instanceof BlazeCWorkspace)) {
       return;
     }
-    rebuildSymbolTables((BlazeCWorkspace) workspace);
+    rebuildSymbolTables(project);
   }
 
-  private static void rebuildSymbolTables(BlazeCWorkspace workspace) {
-    OCWorkspaceModificationTrackers modTrackers = workspace.getModificationTrackers();
+  private static void rebuildSymbolTables(Project project) {
     Transactions.submitTransactionAndWait(
         () ->
             ApplicationManager.getApplication()
                 .runWriteAction(
-                    () -> {
-                      modTrackers.getProjectFilesListTracker().incModificationCount();
-                      modTrackers.getSourceFilesListTracker().incModificationCount();
-                      modTrackers.getBuildConfigurationChangesTracker().incModificationCount();
-                      modTrackers.getBuildSettingsChangesTracker().incModificationCount();
-                    }));
+                    () ->
+                        OCWorkspaceModificationTrackersCompatUtils.incrementModificationCounts(
+                            project)));
   }
 }
diff --git a/ijwb/src/com/google/idea/blaze/ijwb/typescript/BlazeTypescriptSyncPlugin.java b/ijwb/src/com/google/idea/blaze/ijwb/typescript/BlazeTypescriptSyncPlugin.java
index e013c14..d6ab6a7 100644
--- a/ijwb/src/com/google/idea/blaze/ijwb/typescript/BlazeTypescriptSyncPlugin.java
+++ b/ijwb/src/com/google/idea/blaze/ijwb/typescript/BlazeTypescriptSyncPlugin.java
@@ -96,7 +96,9 @@
           childContext.output(new StatusOutput("Updating tsconfig..."));
 
           BlazeCommand command =
-              BlazeCommand.builder(Blaze.getBuildSystem(project), BlazeCommandName.RUN)
+              BlazeCommand.builder(
+                      Blaze.getBuildSystemProvider(project).getSyncBinaryPath(),
+                      BlazeCommandName.RUN)
                   .addTargets(tsConfig)
                   .addBlazeFlags(BlazeFlags.buildFlags(project, projectViewSet))
                   .build();
diff --git a/intellij_platform_sdk/BUILD b/intellij_platform_sdk/BUILD
index 9edcbbf..88f4b35 100644
--- a/intellij_platform_sdk/BUILD
+++ b/intellij_platform_sdk/BUILD
@@ -70,6 +70,14 @@
     },
 )
 
+# Android Studio 2.3.0.6
+config_setting(
+    name = "android-studio-2.3.0.6",
+    values = {
+        "define": "ij_product=android-studio-2.3.0.6",
+    },
+)
+
 config_setting(
     name = "clion-latest",
     values = {
@@ -201,6 +209,9 @@
         "android-studio-2.3.0.4": [
             "missing/tests/com/jetbrains/cidr/modulemap/resolve/MockModuleMapManagerImpl.java",
         ],
+        "android-studio-2.3.0.6": [
+            "missing/tests/com/jetbrains/cidr/modulemap/resolve/MockModuleMapManagerImpl.java",
+        ],
         "default": [],
     }) + ["missing/src/dummy/pkg/DummyClassToAvoidAnEmptyJavaLibrary.java"],
     tags = ["intellij-missing-test-classes"],
diff --git a/intellij_platform_sdk/build_defs.bzl b/intellij_platform_sdk/build_defs.bzl
index 6fa3efb..f833101 100644
--- a/intellij_platform_sdk/build_defs.bzl
+++ b/intellij_platform_sdk/build_defs.bzl
@@ -29,6 +29,10 @@
         ide="android-studio",
         directory="android_studio_2_3_0_4",
     ),
+    "android-studio-2.3.0.6": struct(
+        ide="android-studio",
+        directory="android_studio_2_3_0_6",
+    ),
     "clion-162.1967.7": struct(
         ide="clion",
         directory="CL_162_1967_7",
@@ -119,9 +123,9 @@
       ),
     )
   """
-  intellij = intellij or default
-  android_studio = android_studio or default
-  clion = clion or default
+  intellij = intellij if intellij != None else default
+  android_studio = android_studio if android_studio != None else default
+  clion = clion if clion != None else default
 
   ide_to_value = {
       "intellij" : intellij,
diff --git a/java/BUILD b/java/BUILD
index ac9f772..20786ea 100644
--- a/java/BUILD
+++ b/java/BUILD
@@ -10,7 +10,7 @@
         "//common/experiments",
         "//intellij_platform_sdk:junit",
         "//intellij_platform_sdk:plugin_api",
-        "//proto_deps",
+        "//proto:proto_deps",
         "//sdkcompat",
         "@jsr305_annotations//jar",
     ],
@@ -71,7 +71,7 @@
         "//common/experiments",
         "//common/experiments:unit_test_utils",
         "//intellij_platform_sdk:plugin_api_for_tests",
-        "//proto_deps",
+        "//proto:proto_deps",
         "@jsr305_annotations//jar",
         "@junit//jar",
     ],
diff --git a/java/src/META-INF/blaze-java.xml b/java/src/META-INF/blaze-java.xml
index e46fa6c..bf8b3cf 100644
--- a/java/src/META-INF/blaze-java.xml
+++ b/java/src/META-INF/blaze-java.xml
@@ -84,6 +84,12 @@
     <runConfigurationProducer
         implementation="com.google.idea.blaze.java.run.producers.BlazeJavaTestMethodConfigurationProducer"
         order="first"/>
+    <runConfigurationProducer
+        implementation="com.google.idea.blaze.java.run.producers.BlazeJavaAbstractTestCaseConfigurationProducer"
+        order="first"/>
+    <runConfigurationProducer
+        implementation="com.google.idea.blaze.java.run.producers.MultipleJavaClassesTestConfigurationProducer"
+        order="first"/>
     <projectViewNodeDecorator implementation="com.google.idea.blaze.java.syncstatus.BlazeJavaSyncStatusClassNodeDecorator"/>
     <editorTabColorProvider implementation="com.google.idea.blaze.java.syncstatus.BlazeJavaSyncStatusEditorTabColorProvider"/>
     <editorTabTitleProvider implementation="com.google.idea.blaze.java.syncstatus.BlazeJavaSyncStatusEditorTabTitleProvider"/>
diff --git a/java/src/com/google/idea/blaze/java/lang/build/BuildFileSafeDeleteProcessor.java b/java/src/com/google/idea/blaze/java/lang/build/BuildFileSafeDeleteProcessor.java
index 390ce7b..63b2017 100644
--- a/java/src/com/google/idea/blaze/java/lang/build/BuildFileSafeDeleteProcessor.java
+++ b/java/src/com/google/idea/blaze/java/lang/build/BuildFileSafeDeleteProcessor.java
@@ -30,8 +30,8 @@
 import java.util.Collection;
 import java.util.Iterator;
 import java.util.List;
+import javax.annotation.Nullable;
 import org.jetbrains.annotations.NotNull;
-import org.jetbrains.annotations.Nullable;
 
 /**
  * Removes glob references which don't refer directly to the item(s) being deleted (b/28979434)
diff --git a/java/src/com/google/idea/blaze/java/libraries/LibraryActionHelper.java b/java/src/com/google/idea/blaze/java/libraries/LibraryActionHelper.java
index 35a00ca..cc50651 100644
--- a/java/src/com/google/idea/blaze/java/libraries/LibraryActionHelper.java
+++ b/java/src/com/google/idea/blaze/java/libraries/LibraryActionHelper.java
@@ -31,8 +31,8 @@
 import com.intellij.openapi.ui.Messages;
 import com.intellij.openapi.util.text.StringUtil;
 import com.intellij.pom.Navigatable;
+import javax.annotation.Nullable;
 import org.jetbrains.annotations.NotNull;
-import org.jetbrains.annotations.Nullable;
 
 class LibraryActionHelper {
 
diff --git a/java/src/com/google/idea/blaze/java/run/BlazeJavaRunProfileState.java b/java/src/com/google/idea/blaze/java/run/BlazeJavaRunProfileState.java
index 6175856..b047b41 100644
--- a/java/src/com/google/idea/blaze/java/run/BlazeJavaRunProfileState.java
+++ b/java/src/com/google/idea/blaze/java/run/BlazeJavaRunProfileState.java
@@ -40,7 +40,6 @@
 import com.google.idea.blaze.base.settings.Blaze;
 import com.google.idea.blaze.base.settings.BlazeImportSettings;
 import com.google.idea.blaze.base.settings.BlazeImportSettingsManager;
-import com.google.idea.common.experiments.BoolExperiment;
 import com.intellij.execution.DefaultExecutionResult;
 import com.intellij.execution.ExecutionException;
 import com.intellij.execution.ExecutionResult;
@@ -65,9 +64,6 @@
  */
 final class BlazeJavaRunProfileState extends CommandLineState implements RemoteState {
 
-  private static final BoolExperiment smRunnerUiEnabled =
-      new BoolExperiment("use.smrunner.ui.java", true);
-
   // Blaze seems to always use this port for --java_debug.
   // TODO(joshgiles): Look at manually identifying and setting port.
   private static final int DEBUG_PORT = 5005;
@@ -155,9 +151,6 @@
   }
 
   private boolean useTestUi() {
-    if (!smRunnerUiEnabled.getValue()) {
-      return false;
-    }
     BlazeCommandRunConfigurationCommonState state =
         configuration.getHandlerStateIfType(BlazeCommandRunConfigurationCommonState.class);
     return state != null
@@ -171,10 +164,10 @@
       return null;
     }
     return new RemoteConnection(
-        true /* useSockets */,
+        /* useSockets */ true,
         DEBUG_HOST_NAME,
         Integer.toString(DEBUG_PORT),
-        false /* serverMode */);
+        /* serverMode */ false);
   }
 
   @VisibleForTesting
@@ -189,11 +182,15 @@
         configuration.getHandlerStateIfType(BlazeCommandRunConfigurationCommonState.class);
     assert handlerState != null;
 
+    String binaryPath =
+        handlerState.getBlazeBinary() != null
+            ? handlerState.getBlazeBinary()
+            : Blaze.getBuildSystemProvider(project).getBinaryPath();
+
     BlazeCommandName blazeCommand = handlerState.getCommand();
     assert blazeCommand != null;
     BlazeCommand.Builder command =
-        BlazeCommand.builder(Blaze.getBuildSystem(project), blazeCommand)
-            .setBlazeBinary(handlerState.getBlazeBinary())
+        BlazeCommand.builder(binaryPath, blazeCommand)
             .addTargets(configuration.getTarget())
             .addBlazeFlags(BlazeFlags.buildFlags(project, projectViewSet))
             .addBlazeFlags(extraBlazeFlags)
@@ -210,9 +207,6 @@
     } else {
       boolean runDistributed = handlerState.getRunOnDistributedExecutor();
       command.addBlazeFlags(DistributedExecutorSupport.getBlazeFlags(project, runDistributed));
-      if (!runDistributed) {
-        command.addBlazeFlags(BlazeFlags.TEST_OUTPUT_STREAMED);
-      }
     }
 
     command.addExeFlags(handlerState.getExeFlags());
diff --git a/java/src/com/google/idea/blaze/java/run/BlazeJavaTestEventsHandler.java b/java/src/com/google/idea/blaze/java/run/BlazeJavaTestEventsHandler.java
index 4a1024f..8091b51 100644
--- a/java/src/com/google/idea/blaze/java/run/BlazeJavaTestEventsHandler.java
+++ b/java/src/com/google/idea/blaze/java/run/BlazeJavaTestEventsHandler.java
@@ -47,13 +47,13 @@
 
   /** Overridden to support parameterized tests, which use nested test_suite XML elements. */
   @Override
-  public boolean ignoreSuite(TestSuite suite) {
+  public boolean ignoreSuite(@Nullable Kind kind, TestSuite suite) {
     if (suite.testSuites.isEmpty()) {
       return false;
     }
     for (TestSuite child : suite.testSuites) {
       // target/class names are fully-qualified; unqualified names denote parameterized methods
-      if (!child.name.contains(".")) {
+      if (child.name != null && !child.name.contains(".")) {
         return false;
       }
     }
diff --git a/java/src/com/google/idea/blaze/java/run/RunUtil.java b/java/src/com/google/idea/blaze/java/run/RunUtil.java
index 724d72f..2f36e20 100644
--- a/java/src/com/google/idea/blaze/java/run/RunUtil.java
+++ b/java/src/com/google/idea/blaze/java/run/RunUtil.java
@@ -16,7 +16,7 @@
 package com.google.idea.blaze.java.run;
 
 import com.google.idea.blaze.base.ideinfo.TargetIdeInfo;
-import com.google.idea.blaze.base.ideinfo.TestIdeInfo;
+import com.google.idea.blaze.base.ideinfo.TestIdeInfo.TestSize;
 import com.google.idea.blaze.base.model.primitives.Label;
 import com.google.idea.blaze.base.run.TestTargetFinder;
 import com.google.idea.blaze.base.run.TestTargetHeuristic;
@@ -27,8 +27,7 @@
 import com.intellij.psi.PsiFile;
 import java.io.File;
 import java.util.Collection;
-import org.jetbrains.annotations.NotNull;
-import org.jetbrains.annotations.Nullable;
+import javax.annotation.Nullable;
 
 /** Utility methods for finding rules and Android facets. */
 public final class RunUtil {
@@ -40,18 +39,16 @@
    *     containing rules, the first rule sorted alphabetically by label.
    */
   @Nullable
-  public static TargetIdeInfo targetForTestClass(
-      @NotNull Project project,
-      @NotNull PsiClass testClass,
-      @Nullable TestIdeInfo.TestSize testSize) {
+  public static TargetIdeInfo targetForTestClass(PsiClass testClass, @Nullable TestSize testSize) {
     File testFile = getFileForClass(testClass);
     if (testFile == null) {
       return null;
     }
+    Project project = testClass.getProject();
     Collection<TargetIdeInfo> targets =
         TestTargetFinder.getInstance(project).testTargetsForSourceFile(testFile);
     Label testLabel =
-        TestTargetHeuristic.chooseTestTargetForSourceFile(testFile, targets, testSize);
+        TestTargetHeuristic.chooseTestTargetForSourceFile(project, testFile, targets, testSize);
     if (testLabel == null) {
       return null;
     }
@@ -64,7 +61,7 @@
    * memory.
    */
   @Nullable
-  public static File getFileForClass(@NotNull PsiClass aClass) {
+  public static File getFileForClass(PsiClass aClass) {
     PsiFile containingFile = aClass.getContainingFile();
     if (containingFile == null) {
       return null;
diff --git a/java/src/com/google/idea/blaze/java/run/producers/BlazeJUnitTestFilterFlags.java b/java/src/com/google/idea/blaze/java/run/producers/BlazeJUnitTestFilterFlags.java
index 4ee0508..8c9963d 100644
--- a/java/src/com/google/idea/blaze/java/run/producers/BlazeJUnitTestFilterFlags.java
+++ b/java/src/com/google/idea/blaze/java/run/producers/BlazeJUnitTestFilterFlags.java
@@ -105,10 +105,12 @@
       String filter =
           testFilterForClassAndMethods(
               entry.getKey(), version, extractMethodFilters(entry.getValue()));
-      if (filter != null) {
-        classFilters.add(filter);
+      if (filter == null) {
+        return null;
       }
+      classFilters.add(filter);
     }
+    classFilters.sort(String::compareTo);
     return version == JUnitVersion.JUNIT_4
         ? String.join("|", classFilters)
         : String.join(",", classFilters);
diff --git a/java/src/com/google/idea/blaze/java/run/producers/BlazeJavaAbstractTestCaseConfigurationProducer.java b/java/src/com/google/idea/blaze/java/run/producers/BlazeJavaAbstractTestCaseConfigurationProducer.java
new file mode 100644
index 0000000..7e6ca2f
--- /dev/null
+++ b/java/src/com/google/idea/blaze/java/run/producers/BlazeJavaAbstractTestCaseConfigurationProducer.java
@@ -0,0 +1,222 @@
+/*
+ * Copyright 2017 The Bazel Authors. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.idea.blaze.java.run.producers;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.collect.ImmutableList;
+import com.google.idea.blaze.base.command.BlazeCommandName;
+import com.google.idea.blaze.base.command.BlazeFlags;
+import com.google.idea.blaze.base.ideinfo.TargetIdeInfo;
+import com.google.idea.blaze.base.ideinfo.TestIdeInfo;
+import com.google.idea.blaze.base.run.BlazeCommandRunConfiguration;
+import com.google.idea.blaze.base.run.BlazeCommandRunConfigurationType;
+import com.google.idea.blaze.base.run.BlazeConfigurationNameBuilder;
+import com.google.idea.blaze.base.run.producers.BlazeRunConfigurationProducer;
+import com.google.idea.blaze.base.run.smrunner.SmRunnerUtils;
+import com.google.idea.blaze.base.run.state.BlazeCommandRunConfigurationCommonState;
+import com.google.idea.blaze.java.run.RunUtil;
+import com.intellij.codeInsight.AnnotationUtil;
+import com.intellij.execution.JavaExecutionUtil;
+import com.intellij.execution.Location;
+import com.intellij.execution.actions.ConfigurationContext;
+import com.intellij.execution.actions.ConfigurationFromContext;
+import com.intellij.execution.configurations.RunConfiguration;
+import com.intellij.execution.junit.JUnitUtil;
+import com.intellij.openapi.util.Ref;
+import com.intellij.psi.PsiClass;
+import com.intellij.psi.PsiElement;
+import com.intellij.psi.PsiMethod;
+import com.intellij.psi.PsiModifier;
+import com.intellij.psi.util.PsiTreeUtil;
+import java.util.ArrayList;
+import java.util.List;
+import javax.annotation.Nullable;
+
+/** Producer for abstract test classes/methods. */
+public class BlazeJavaAbstractTestCaseConfigurationProducer
+    extends BlazeRunConfigurationProducer<BlazeCommandRunConfiguration> {
+
+  private static class AbstractTestLocation {
+    private final PsiClass abstractClass;
+    @Nullable private final PsiMethod method;
+
+    private AbstractTestLocation(PsiClass abstractClass, @Nullable PsiMethod method) {
+      this.abstractClass = abstractClass;
+      this.method = method;
+    }
+  }
+
+  public BlazeJavaAbstractTestCaseConfigurationProducer() {
+    super(BlazeCommandRunConfigurationType.getInstance());
+  }
+
+  @Override
+  protected boolean doSetupConfigFromContext(
+      BlazeCommandRunConfiguration configuration,
+      ConfigurationContext context,
+      Ref<PsiElement> sourceElement) {
+    AbstractTestLocation location = getAbstractLocation(context);
+    if (location == null) {
+      return false;
+    }
+    sourceElement.set(location.method != null ? location.method : location.abstractClass);
+    configuration.setName(configName(location.abstractClass, location.method));
+    configuration.setNameChangedByUser(true);
+    return true;
+  }
+
+  @Nullable
+  private static AbstractTestLocation getAbstractLocation(ConfigurationContext context) {
+    if (!SmRunnerUtils.getSelectedSmRunnerTreeElements(context).isEmpty()) {
+      // handled by a different producer
+      return null;
+    }
+    PsiMethod method = getTestMethod(context);
+    if (method != null) {
+      PsiClass psiClass = method.getContainingClass();
+      return isAbstractClass(psiClass) ? new AbstractTestLocation(psiClass, method) : null;
+    }
+    Location location = context.getLocation();
+    if (location == null) {
+      return null;
+    }
+    location = JavaExecutionUtil.stepIntoSingleClass(location);
+    PsiClass psiClass =
+        PsiTreeUtil.getParentOfType(location.getPsiElement(), PsiClass.class, false);
+    if (!isAbstractClass(psiClass)) {
+      return null;
+    }
+    for (PsiClass subclass : SubclassTestChooser.findTestSubclasses(psiClass)) {
+      if (JUnitUtil.isTestClass(subclass)) {
+        return new AbstractTestLocation(psiClass, null);
+      }
+    }
+    return null;
+  }
+
+  private static PsiMethod getTestMethod(ConfigurationContext context) {
+    PsiElement psi = context.getPsiLocation();
+    if (psi instanceof PsiMethod
+        && AnnotationUtil.isAnnotated((PsiMethod) psi, JUnitUtil.TEST_ANNOTATION, false)) {
+      return (PsiMethod) psi;
+    }
+    List<PsiMethod> selectedMethods = TestMethodSelectionUtil.getSelectedMethods(context);
+    return selectedMethods != null && selectedMethods.size() == 1 ? selectedMethods.get(0) : null;
+  }
+
+  private static boolean isAbstractClass(@Nullable PsiClass psiClass) {
+    return psiClass != null && psiClass.hasModifierProperty(PsiModifier.ABSTRACT);
+  }
+
+  @Override
+  protected boolean doIsConfigFromContext(
+      BlazeCommandRunConfiguration configuration, ConfigurationContext context) {
+    // this is an intermediate type -- when it's fully instantiated (via 'onFirstRun') it will be
+    // recognized by a different producer.
+    return false;
+  }
+
+  @Override
+  public void onFirstRun(
+      ConfigurationFromContext configuration,
+      ConfigurationContext context,
+      Runnable startRunnable) {
+    chooseSubclass(configuration, context, startRunnable);
+  }
+
+  @VisibleForTesting
+  static void chooseSubclass(
+      ConfigurationFromContext configuration,
+      ConfigurationContext context,
+      Runnable startRunnable) {
+    RunConfiguration config = configuration.getConfiguration();
+    if (!(config instanceof BlazeCommandRunConfiguration)) {
+      return;
+    }
+    AbstractTestLocation location = locationFromConfiguration(configuration);
+    if (location == null) {
+      return;
+    }
+    SubclassTestChooser.chooseSubclass(
+        context,
+        location.abstractClass,
+        (psiClass) -> {
+          if (psiClass != null) {
+            setupContext((BlazeCommandRunConfiguration) config, psiClass, location.method);
+          }
+          startRunnable.run();
+        });
+  }
+
+  @Nullable
+  private static AbstractTestLocation locationFromConfiguration(
+      ConfigurationFromContext configuration) {
+    PsiElement element = configuration.getSourceElement();
+    PsiMethod method = null;
+    PsiClass psiClass = null;
+    if (element instanceof PsiMethod) {
+      method = (PsiMethod) element;
+      psiClass = method.getContainingClass();
+    } else if (element instanceof PsiClass) {
+      psiClass = (PsiClass) element;
+    }
+
+    return isAbstractClass(psiClass) ? new AbstractTestLocation(psiClass, method) : null;
+  }
+
+  private static void setupContext(
+      BlazeCommandRunConfiguration configuration, PsiClass subClass, @Nullable PsiMethod method) {
+    TestIdeInfo.TestSize testSize =
+        method != null
+            ? TestSizeAnnotationMap.getTestSize(method)
+            : TestSizeAnnotationMap.getTestSize(subClass);
+    TargetIdeInfo target = RunUtil.targetForTestClass(subClass, testSize);
+    if (target == null) {
+      return;
+    }
+    configuration.setTarget(target.key.label);
+    BlazeCommandRunConfigurationCommonState handlerState =
+        configuration.getHandlerStateIfType(BlazeCommandRunConfigurationCommonState.class);
+    if (handlerState == null) {
+      return;
+    }
+    handlerState.setCommand(BlazeCommandName.TEST);
+
+    // remove old test filter flag if present
+    List<String> flags = new ArrayList<>(handlerState.getBlazeFlags());
+    flags.removeIf((flag) -> flag.startsWith(BlazeFlags.TEST_FILTER));
+
+    String testFilter =
+        BlazeJUnitTestFilterFlags.testFilterForClassAndMethods(
+            subClass, method == null ? ImmutableList.of() : ImmutableList.of(method));
+    if (testFilter == null) {
+      return;
+    }
+    flags.add(BlazeFlags.TEST_FILTER + "=" + testFilter);
+    handlerState.setBlazeFlags(flags);
+
+    BlazeConfigurationNameBuilder nameBuilder = new BlazeConfigurationNameBuilder(configuration);
+    nameBuilder.setTargetString(configName(subClass, method));
+    configuration.setName(nameBuilder.build());
+    configuration.setNameChangedByUser(true); // don't revert to generated name
+  }
+
+  private static String configName(PsiClass psiClass, @Nullable PsiMethod method) {
+    return method == null
+        ? psiClass.getName()
+        : String.format("%s.%s", psiClass.getName(), method.getName());
+  }
+}
diff --git a/java/src/com/google/idea/blaze/java/run/producers/BlazeJavaMainClassRunConfigurationProducer.java b/java/src/com/google/idea/blaze/java/run/producers/BlazeJavaMainClassRunConfigurationProducer.java
index b90826a..73827e8 100644
--- a/java/src/com/google/idea/blaze/java/run/producers/BlazeJavaMainClassRunConfigurationProducer.java
+++ b/java/src/com/google/idea/blaze/java/run/producers/BlazeJavaMainClassRunConfigurationProducer.java
@@ -41,7 +41,7 @@
 import java.io.File;
 import java.util.Collection;
 import java.util.Objects;
-import org.jetbrains.annotations.Nullable;
+import javax.annotation.Nullable;
 
 /** Creates run configurations for Java main classes sourced by java_binary targets. */
 public class BlazeJavaMainClassRunConfigurationProducer
diff --git a/java/src/com/google/idea/blaze/java/run/producers/BlazeJavaTestClassConfigurationProducer.java b/java/src/com/google/idea/blaze/java/run/producers/BlazeJavaTestClassConfigurationProducer.java
index 7d9a20f..20d6b74 100644
--- a/java/src/com/google/idea/blaze/java/run/producers/BlazeJavaTestClassConfigurationProducer.java
+++ b/java/src/com/google/idea/blaze/java/run/producers/BlazeJavaTestClassConfigurationProducer.java
@@ -79,7 +79,7 @@
     sourceElement.set(testClass);
 
     TestIdeInfo.TestSize testSize = TestSizeAnnotationMap.getTestSize(testClass);
-    TargetIdeInfo target = RunUtil.targetForTestClass(context.getProject(), testClass, testSize);
+    TargetIdeInfo target = RunUtil.targetForTestClass(testClass, testSize);
     if (target == null) {
       return false;
     }
diff --git a/java/src/com/google/idea/blaze/java/run/producers/BlazeJavaTestMethodConfigurationProducer.java b/java/src/com/google/idea/blaze/java/run/producers/BlazeJavaTestMethodConfigurationProducer.java
index e0453d1..071d5fb 100644
--- a/java/src/com/google/idea/blaze/java/run/producers/BlazeJavaTestMethodConfigurationProducer.java
+++ b/java/src/com/google/idea/blaze/java/run/producers/BlazeJavaTestMethodConfigurationProducer.java
@@ -80,8 +80,7 @@
     sourceElement.set(methodInfo.firstMethod);
 
     TestIdeInfo.TestSize testSize = TestSizeAnnotationMap.getTestSize(methodInfo.firstMethod);
-    TargetIdeInfo target =
-        RunUtil.targetForTestClass(context.getProject(), methodInfo.containingClass, testSize);
+    TargetIdeInfo target = RunUtil.targetForTestClass(methodInfo.containingClass, testSize);
     if (target == null) {
       return false;
     }
diff --git a/java/src/com/google/idea/blaze/java/run/producers/MultipleJavaClassesTestConfigurationProducer.java b/java/src/com/google/idea/blaze/java/run/producers/MultipleJavaClassesTestConfigurationProducer.java
new file mode 100644
index 0000000..df62114
--- /dev/null
+++ b/java/src/com/google/idea/blaze/java/run/producers/MultipleJavaClassesTestConfigurationProducer.java
@@ -0,0 +1,239 @@
+/*
+ * Copyright 2017 The Bazel Authors. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.idea.blaze.java.run.producers;
+
+import com.google.common.collect.ImmutableList;
+import com.google.idea.blaze.base.command.BlazeCommandName;
+import com.google.idea.blaze.base.command.BlazeFlags;
+import com.google.idea.blaze.base.ideinfo.TargetIdeInfo;
+import com.google.idea.blaze.base.ideinfo.TestIdeInfo;
+import com.google.idea.blaze.base.lang.buildfile.search.BlazePackage;
+import com.google.idea.blaze.base.model.primitives.Label;
+import com.google.idea.blaze.base.run.BlazeCommandRunConfiguration;
+import com.google.idea.blaze.base.run.BlazeCommandRunConfigurationType;
+import com.google.idea.blaze.base.run.BlazeConfigurationNameBuilder;
+import com.google.idea.blaze.base.run.producers.BlazeRunConfigurationProducer;
+import com.google.idea.blaze.base.run.smrunner.SmRunnerUtils;
+import com.google.idea.blaze.base.run.state.BlazeCommandRunConfigurationCommonState;
+import com.google.idea.blaze.java.run.RunUtil;
+import com.intellij.execution.Location;
+import com.intellij.execution.actions.ConfigurationContext;
+import com.intellij.execution.junit.JUnitUtil;
+import com.intellij.openapi.actionSystem.DataContext;
+import com.intellij.openapi.actionSystem.LangDataKeys;
+import com.intellij.openapi.roots.ProjectFileIndex;
+import com.intellij.openapi.util.Ref;
+import com.intellij.psi.JavaDirectoryService;
+import com.intellij.psi.PsiClass;
+import com.intellij.psi.PsiDirectory;
+import com.intellij.psi.PsiElement;
+import com.intellij.psi.PsiModifier;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.stream.Collectors;
+import javax.annotation.Nullable;
+
+/**
+ * Runs tests in all selected java classes (or all classes below selected directory). Ignores
+ * classes spread across multiple test targets.
+ */
+public class MultipleJavaClassesTestConfigurationProducer
+    extends BlazeRunConfigurationProducer<BlazeCommandRunConfiguration> {
+
+  public MultipleJavaClassesTestConfigurationProducer() {
+    super(BlazeCommandRunConfigurationType.getInstance());
+  }
+
+  @Override
+  protected boolean doSetupConfigFromContext(
+      BlazeCommandRunConfiguration configuration,
+      ConfigurationContext context,
+      Ref<PsiElement> sourceElement) {
+    TestLocation location = getTestLocation(context);
+    if (location == null) {
+      return false;
+    }
+    sourceElement.set(location.psiLocation);
+    configuration.setTarget(location.label);
+    BlazeCommandRunConfigurationCommonState handlerState =
+        configuration.getHandlerStateIfType(BlazeCommandRunConfigurationCommonState.class);
+    if (handlerState == null) {
+      return false;
+    }
+    handlerState.setCommand(BlazeCommandName.TEST);
+
+    // remove old test filter flag if present
+    List<String> flags = new ArrayList<>(handlerState.getBlazeFlags());
+    flags.removeIf((flag) -> flag.startsWith(BlazeFlags.TEST_FILTER));
+    if (location.testFilter != null) {
+      flags.add(location.testFilter);
+    }
+    handlerState.setBlazeFlags(flags);
+
+    if (location.description != null) {
+      BlazeConfigurationNameBuilder nameBuilder = new BlazeConfigurationNameBuilder(configuration);
+      nameBuilder.setTargetString(location.description);
+      configuration.setName(nameBuilder.build());
+      configuration.setNameChangedByUser(true); // don't revert to generated name
+    } else {
+      configuration.setGeneratedName();
+    }
+    return true;
+  }
+
+  @Override
+  protected boolean doIsConfigFromContext(
+      BlazeCommandRunConfiguration configuration, ConfigurationContext context) {
+
+    TestLocation location = getTestLocation(context);
+    if (location == null) {
+      return false;
+    }
+    BlazeCommandRunConfigurationCommonState handlerState =
+        configuration.getHandlerStateIfType(BlazeCommandRunConfigurationCommonState.class);
+    if (handlerState == null) {
+      return false;
+    }
+    return BlazeCommandName.TEST.equals(handlerState.getCommand())
+        && location.label.equals(configuration.getTarget())
+        && Objects.equals(location.testFilter, handlerState.getTestFilterFlag());
+  }
+
+  @Nullable
+  private static TestLocation getTestLocation(ConfigurationContext context) {
+    if (!SmRunnerUtils.getSelectedSmRunnerTreeElements(context).isEmpty()) {
+      // handled by a different producer
+      return null;
+    }
+    PsiElement location = context.getPsiLocation();
+    if (location instanceof PsiDirectory) {
+      PsiDirectory dir = (PsiDirectory) location;
+      TargetIdeInfo target = getTestTargetIfUnique(dir);
+      return target != null ? TestLocation.fromDirectory(target.key.label, dir) : null;
+    }
+    List<PsiClass> testClasses = selectedTestClasses(context);
+    if (testClasses.size() < 2) {
+      return null;
+    }
+    TargetIdeInfo target = getTestTargetIfUnique(testClasses);
+    return target != null ? TestLocation.fromClasses(target.key.label, testClasses) : null;
+  }
+
+  private static List<PsiClass> selectedTestClasses(ConfigurationContext context) {
+    DataContext dataContext = context.getDataContext();
+    PsiElement[] elements = LangDataKeys.PSI_ELEMENT_ARRAY.getData(dataContext);
+    if (elements == null) {
+      return ImmutableList.of();
+    }
+    return Arrays.stream(elements)
+        .map(JUnitUtil::getTestClass)
+        .filter(Objects::nonNull)
+        .filter(testClass -> !testClass.hasModifierProperty(PsiModifier.ABSTRACT))
+        .collect(Collectors.toList());
+  }
+
+  @Nullable
+  private static TargetIdeInfo getTestTargetIfUnique(PsiDirectory directory) {
+    if (BlazePackage.hasBlazePackageChild(directory)) {
+      return null;
+    }
+    List<PsiClass> classes = new ArrayList<>();
+    addClassesInDirectory(directory, classes);
+    return getTestTargetIfUnique(classes);
+  }
+
+  private static void addClassesInDirectory(PsiDirectory directory, List<PsiClass> list) {
+    Collections.addAll(list, JavaDirectoryService.getInstance().getClasses(directory));
+    for (PsiDirectory child : directory.getSubdirectories()) {
+      addClassesInDirectory(child, list);
+    }
+  }
+
+  @Nullable
+  private static TargetIdeInfo getTestTargetIfUnique(List<PsiClass> classes) {
+    TargetIdeInfo testTarget = null;
+    for (PsiClass psiClass : classes) {
+      TargetIdeInfo target = testTargetForClass(psiClass);
+      if (target == null) {
+        continue;
+      }
+      if (testTarget != null && testTarget != target) {
+        return null;
+      }
+      testTarget = target;
+    }
+    return testTarget;
+  }
+
+  @Nullable
+  private static TargetIdeInfo testTargetForClass(PsiClass psiClass) {
+    PsiClass testClass = JUnitUtil.getTestClass(psiClass);
+    if (testClass == null || testClass.hasModifierProperty(PsiModifier.ABSTRACT)) {
+      return null;
+    }
+    TestIdeInfo.TestSize testSize = TestSizeAnnotationMap.getTestSize(psiClass);
+    return RunUtil.targetForTestClass(psiClass, testSize);
+  }
+
+  private static class TestLocation {
+    @Nullable
+    static TestLocation fromClasses(Label label, List<PsiClass> classes) {
+      Map<PsiClass, Collection<Location<?>>> methodsPerClass =
+          classes.stream().collect(Collectors.toMap(c -> c, c -> ImmutableList.of()));
+      String filter = BlazeJUnitTestFilterFlags.testFilterForClassesAndMethods(methodsPerClass);
+      if (filter == null) {
+        return null;
+      }
+      String name = classes.get(0).getName();
+      if (classes.size() > 1) {
+        name += String.format(" and %s others", classes.size() - 1);
+      }
+      return new TestLocation(label, classes.get(0), filter, name);
+    }
+
+    @Nullable
+    static TestLocation fromDirectory(Label label, PsiDirectory dir) {
+      String packagePrefix =
+          ProjectFileIndex.SERVICE
+              .getInstance(dir.getProject())
+              .getPackageNameByDirectory(dir.getVirtualFile());
+      if (packagePrefix == null) {
+        return null;
+      }
+      String description =
+          packagePrefix.isEmpty() ? null : String.format("all in directory '%s'", dir.getName());
+      return new TestLocation(label, dir, packagePrefix, description);
+    }
+
+    private final Label label;
+    private final PsiElement psiLocation;
+    @Nullable private final String testFilter;
+    @Nullable private final String description;
+
+    private TestLocation(
+        Label label, PsiElement psiLocation, String testFilter, @Nullable String description) {
+      this.label = label;
+      this.psiLocation = psiLocation;
+      this.testFilter = !testFilter.isEmpty() ? BlazeFlags.TEST_FILTER + "=" + testFilter : null;
+      this.description = description;
+    }
+  }
+}
diff --git a/java/src/com/google/idea/blaze/java/run/producers/NonBlazeProducerSuppressor.java b/java/src/com/google/idea/blaze/java/run/producers/NonBlazeProducerSuppressor.java
index faf3545..3d64ef8 100644
--- a/java/src/com/google/idea/blaze/java/run/producers/NonBlazeProducerSuppressor.java
+++ b/java/src/com/google/idea/blaze/java/run/producers/NonBlazeProducerSuppressor.java
@@ -35,17 +35,18 @@
   private static final Collection<Class<? extends RunConfigurationProducer<?>>>
       PRODUCERS_TO_SUPPRESS =
           ImmutableList.of(
-              // JUnit test producers
               com.intellij.execution.junit.AllInDirectoryConfigurationProducer.class,
               com.intellij.execution.junit.AllInPackageConfigurationProducer.class,
               com.intellij.execution.junit.TestClassConfigurationProducer.class,
               com.intellij.execution.junit.TestMethodConfigurationProducer.class,
-              com.intellij.execution.junit.PatternConfigurationProducer.class);
+              com.intellij.execution.junit.PatternConfigurationProducer.class,
+              com.intellij.execution.application.ApplicationConfigurationProducer.class);
 
-  private static final ImmutableList<String> KOTLIN_JUNIT_PRODUCERS =
+  private static final ImmutableList<String> KOTLIN_PRODUCERS =
       ImmutableList.of(
           "org.jetbrains.kotlin.idea.run.KotlinJUnitRunConfigurationProducer",
-          "org.jetbrains.kotlin.idea.run.KotlinPatternConfigurationProducer");
+          "org.jetbrains.kotlin.idea.run.KotlinPatternConfigurationProducer",
+          "org.jetbrains.kotlin.idea.run.KotlinRunConfigurationProducer");
 
   private static Collection<Class<? extends RunConfigurationProducer<?>>> getKotlinProducers() {
     // rather than compiling against the Kotlin plugin, and including a switch in the our
@@ -55,7 +56,7 @@
       return ImmutableList.of();
     }
     ClassLoader loader = plugin.getPluginClassLoader();
-    return KOTLIN_JUNIT_PRODUCERS
+    return KOTLIN_PRODUCERS
         .stream()
         .map((qualifiedName) -> loadClass(loader, qualifiedName))
         .filter(Objects::nonNull)
diff --git a/java/src/com/google/idea/blaze/java/run/producers/ProducerUtils.java b/java/src/com/google/idea/blaze/java/run/producers/ProducerUtils.java
index b1b40ab..e7d809a 100644
--- a/java/src/com/google/idea/blaze/java/run/producers/ProducerUtils.java
+++ b/java/src/com/google/idea/blaze/java/run/producers/ProducerUtils.java
@@ -22,8 +22,8 @@
 import com.intellij.psi.PsiClass;
 import com.intellij.psi.PsiMethod;
 import java.util.Iterator;
+import javax.annotation.Nullable;
 import org.jetbrains.annotations.NotNull;
-import org.jetbrains.annotations.Nullable;
 
 /**
  * Copy of {@link org.jetbrains.plugins.gradle.execution.test.runner.TestRunnerUtils}.
diff --git a/java/src/com/google/idea/blaze/java/run/producers/SubclassTestChooser.java b/java/src/com/google/idea/blaze/java/run/producers/SubclassTestChooser.java
new file mode 100644
index 0000000..1bc5250
--- /dev/null
+++ b/java/src/com/google/idea/blaze/java/run/producers/SubclassTestChooser.java
@@ -0,0 +1,73 @@
+/*
+ * Copyright 2017 The Bazel Authors. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.idea.blaze.java.run.producers;
+
+import com.intellij.execution.actions.ConfigurationContext;
+import com.intellij.execution.junit.JUnitUtil;
+import com.intellij.ide.util.PsiClassListCellRenderer;
+import com.intellij.openapi.ui.popup.JBPopupFactory;
+import com.intellij.psi.PsiClass;
+import com.intellij.psi.search.searches.ClassInheritorsSearch;
+import com.intellij.ui.components.JBList;
+import java.util.List;
+import java.util.function.Consumer;
+import java.util.stream.Collectors;
+import javax.swing.ListSelectionModel;
+
+/**
+ * Pop up a dialog to choose a child test class. Called when creating a run configuration from an
+ * abstract test class/method.
+ */
+public class SubclassTestChooser {
+
+  static void chooseSubclass(
+      ConfigurationContext context,
+      PsiClass abstractClass,
+      Consumer<PsiClass> callbackOnClassSelection) {
+    List<PsiClass> classes = findTestSubclasses(abstractClass);
+    if (classes.isEmpty()) {
+      return;
+    }
+    if (classes.size() == 1) {
+      callbackOnClassSelection.accept(classes.get(0));
+      return;
+    }
+    PsiClassListCellRenderer renderer = new PsiClassListCellRenderer();
+    classes.sort(renderer.getComparator());
+    // JBList has no generics in AS 2.2. TODO: Add generics here when we migrate to AS 2.3.
+    JBList list = new JBList(classes);
+    list.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
+    list.setCellRenderer(renderer);
+    JBPopupFactory.getInstance()
+        .createListPopupBuilder(list)
+        .setTitle("Choose test class to run")
+        .setMovable(false)
+        .setResizable(false)
+        .setRequestFocus(true)
+        .setItemChoosenCallback(
+            () -> callbackOnClassSelection.accept((PsiClass) list.getSelectedValue()))
+        .createPopup()
+        .showInBestPositionFor(context.getDataContext());
+  }
+
+  static List<PsiClass> findTestSubclasses(PsiClass abstractClass) {
+    return ClassInheritorsSearch.search(abstractClass)
+        .findAll()
+        .stream()
+        .filter(JUnitUtil::isTestClass)
+        .collect(Collectors.toList());
+  }
+}
diff --git a/java/src/com/google/idea/blaze/java/run/producers/TestMethodSelectionUtil.java b/java/src/com/google/idea/blaze/java/run/producers/TestMethodSelectionUtil.java
index e19f9bf..3db22aa 100644
--- a/java/src/com/google/idea/blaze/java/run/producers/TestMethodSelectionUtil.java
+++ b/java/src/com/google/idea/blaze/java/run/producers/TestMethodSelectionUtil.java
@@ -27,8 +27,8 @@
 import java.util.Collections;
 import java.util.Iterator;
 import java.util.List;
+import javax.annotation.Nullable;
 import org.jetbrains.annotations.NotNull;
-import org.jetbrains.annotations.Nullable;
 
 /** Helper functions for getting selected test methods. */
 public class TestMethodSelectionUtil {
diff --git a/java/src/com/google/idea/blaze/java/run/producers/TestSizeAnnotationMap.java b/java/src/com/google/idea/blaze/java/run/producers/TestSizeAnnotationMap.java
index 13062b3..66cb44f 100644
--- a/java/src/com/google/idea/blaze/java/run/producers/TestSizeAnnotationMap.java
+++ b/java/src/com/google/idea/blaze/java/run/producers/TestSizeAnnotationMap.java
@@ -25,7 +25,7 @@
 
 /** Maps method and class annotations to our test size enumeration. */
 public class TestSizeAnnotationMap {
-  private static ImmutableMap<String, TestIdeInfo.TestSize> ANNOTATION_TO_TEST_SIZE =
+  private static final ImmutableMap<String, TestIdeInfo.TestSize> ANNOTATION_TO_TEST_SIZE =
       ImmutableMap.<String, TestIdeInfo.TestSize>builder()
           .put("com.google.testing.testsize.SmallTest", TestIdeInfo.TestSize.SMALL)
           .put("com.google.testing.testsize.MediumTest", TestIdeInfo.TestSize.MEDIUM)
diff --git a/java/src/com/google/idea/blaze/java/sync/jdeps/JdepsMap.java b/java/src/com/google/idea/blaze/java/sync/jdeps/JdepsMap.java
index ac53134..b58b8cc 100644
--- a/java/src/com/google/idea/blaze/java/sync/jdeps/JdepsMap.java
+++ b/java/src/com/google/idea/blaze/java/sync/jdeps/JdepsMap.java
@@ -17,7 +17,7 @@
 
 import com.google.idea.blaze.base.ideinfo.TargetKey;
 import java.util.List;
-import org.jetbrains.annotations.Nullable;
+import javax.annotation.Nullable;
 
 /** Map of rule -> jdeps dependencies. */
 public interface JdepsMap {
diff --git a/java/src/com/google/idea/blaze/java/sync/projectstructure/Jdks.java b/java/src/com/google/idea/blaze/java/sync/projectstructure/Jdks.java
index e80c98b..704f4c4 100644
--- a/java/src/com/google/idea/blaze/java/sync/projectstructure/Jdks.java
+++ b/java/src/com/google/idea/blaze/java/sync/projectstructure/Jdks.java
@@ -36,9 +36,9 @@
 import java.util.Collection;
 import java.util.Iterator;
 import java.util.List;
+import javax.annotation.Nullable;
 import org.jetbrains.annotations.NonNls;
 import org.jetbrains.annotations.NotNull;
-import org.jetbrains.annotations.Nullable;
 
 /** Utility methods related to IDEA JDKs. */
 public class Jdks {
diff --git a/java/src/com/google/idea/blaze/java/syncstatus/BlazeJavaSyncStatusEditorTabColorProvider.java b/java/src/com/google/idea/blaze/java/syncstatus/BlazeJavaSyncStatusEditorTabColorProvider.java
index 91234b1..b3e3b2e 100644
--- a/java/src/com/google/idea/blaze/java/syncstatus/BlazeJavaSyncStatusEditorTabColorProvider.java
+++ b/java/src/com/google/idea/blaze/java/syncstatus/BlazeJavaSyncStatusEditorTabColorProvider.java
@@ -20,8 +20,8 @@
 import com.intellij.openapi.vfs.VirtualFile;
 import com.intellij.ui.JBColor;
 import java.awt.Color;
+import javax.annotation.Nullable;
 import org.jetbrains.annotations.NotNull;
-import org.jetbrains.annotations.Nullable;
 
 /** Changes the color for unsynced files. */
 public class BlazeJavaSyncStatusEditorTabColorProvider implements EditorTabColorProvider {
diff --git a/java/src/com/google/idea/blaze/java/syncstatus/BlazeJavaSyncStatusEditorTabTitleProvider.java b/java/src/com/google/idea/blaze/java/syncstatus/BlazeJavaSyncStatusEditorTabTitleProvider.java
index aa96de8..6597752 100644
--- a/java/src/com/google/idea/blaze/java/syncstatus/BlazeJavaSyncStatusEditorTabTitleProvider.java
+++ b/java/src/com/google/idea/blaze/java/syncstatus/BlazeJavaSyncStatusEditorTabTitleProvider.java
@@ -18,7 +18,7 @@
 import com.intellij.openapi.fileEditor.impl.EditorTabTitleProvider;
 import com.intellij.openapi.project.Project;
 import com.intellij.openapi.vfs.VirtualFile;
-import org.jetbrains.annotations.Nullable;
+import javax.annotation.Nullable;
 
 /** Changes the tab title for unsynced files. */
 public class BlazeJavaSyncStatusEditorTabTitleProvider implements EditorTabTitleProvider {
diff --git a/java/src/com/google/idea/blaze/java/ui/BlazeIntelliJProblemsView.java b/java/src/com/google/idea/blaze/java/ui/BlazeIntelliJProblemsView.java
index a2f0582..2c58b33 100644
--- a/java/src/com/google/idea/blaze/java/ui/BlazeIntelliJProblemsView.java
+++ b/java/src/com/google/idea/blaze/java/ui/BlazeIntelliJProblemsView.java
@@ -41,7 +41,7 @@
   public void addMessage(IssueOutput issue, UUID sessionId) {
     VirtualFile virtualFile =
         issue.getFile() != null
-            ? VfsUtil.findFileByIoFile(issue.getFile(), true /* refresh */)
+            ? VfsUtil.findFileByIoFile(issue.getFile(), /* refresh */ true)
             : null;
     CompilerMessageCategory category =
         issue.getCategory() == IssueOutput.Category.ERROR
diff --git a/java/src/com/google/idea/blaze/java/wizard2/BlazeNewProjectWizard.java b/java/src/com/google/idea/blaze/java/wizard2/BlazeNewProjectWizard.java
index 62be22e..b609203 100644
--- a/java/src/com/google/idea/blaze/java/wizard2/BlazeNewProjectWizard.java
+++ b/java/src/com/google/idea/blaze/java/wizard2/BlazeNewProjectWizard.java
@@ -19,7 +19,7 @@
 import com.intellij.ide.util.newProjectWizard.AddModuleWizard;
 import com.intellij.projectImport.ProjectImportProvider;
 import java.awt.event.ActionListener;
-import org.jetbrains.annotations.Nullable;
+import javax.annotation.Nullable;
 
 final class BlazeNewProjectWizard extends AddModuleWizard {
   public BlazeNewProjectWizard(ProjectImportProvider provider) {
diff --git a/java/tests/integrationtests/com/google/idea/blaze/java/run/producers/BlazeJavaAbstractTestCaseConfigurationProducerTest.java b/java/tests/integrationtests/com/google/idea/blaze/java/run/producers/BlazeJavaAbstractTestCaseConfigurationProducerTest.java
new file mode 100644
index 0000000..b30fa3e
--- /dev/null
+++ b/java/tests/integrationtests/com/google/idea/blaze/java/run/producers/BlazeJavaAbstractTestCaseConfigurationProducerTest.java
@@ -0,0 +1,194 @@
+/*
+ * Copyright 2017 The Bazel Authors. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.idea.blaze.java.run.producers;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.google.idea.blaze.base.command.BlazeFlags;
+import com.google.idea.blaze.base.ideinfo.TargetIdeInfo;
+import com.google.idea.blaze.base.ideinfo.TargetMapBuilder;
+import com.google.idea.blaze.base.lang.buildfile.psi.util.PsiUtils;
+import com.google.idea.blaze.base.model.MockBlazeProjectDataBuilder;
+import com.google.idea.blaze.base.model.MockBlazeProjectDataManager;
+import com.google.idea.blaze.base.model.primitives.TargetExpression;
+import com.google.idea.blaze.base.model.primitives.WorkspacePath;
+import com.google.idea.blaze.base.run.BlazeCommandRunConfiguration;
+import com.google.idea.blaze.base.run.producer.BlazeRunConfigurationProducerTestCase;
+import com.google.idea.blaze.base.sync.data.BlazeProjectDataManager;
+import com.intellij.execution.actions.ConfigurationContext;
+import com.intellij.execution.actions.ConfigurationFromContext;
+import com.intellij.execution.configurations.RunConfiguration;
+import com.intellij.openapi.util.EmptyRunnable;
+import com.intellij.psi.PsiClass;
+import com.intellij.psi.PsiClassOwner;
+import com.intellij.psi.PsiFile;
+import com.intellij.psi.PsiMethod;
+import java.util.List;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** Integration tests for {@link BlazeJavaAbstractTestCaseConfigurationProducer}. */
+@RunWith(JUnit4.class)
+public class BlazeJavaAbstractTestCaseConfigurationProducerTest
+    extends BlazeRunConfigurationProducerTestCase {
+
+  @Test
+  public void testNonAbstractClassIgnored() {
+    PsiFile javaFile =
+        createAndIndexFile(
+            new WorkspacePath("java/com/google/test/TestClass.java"),
+            "package com.google.test;",
+            "@org.junit.runner.RunWith(org.junit.runners.JUnit4.class)",
+            "public class TestClass {",
+            "  @org.junit.Test",
+            "  public void testMethod1() {}",
+            "  @org.junit.Test",
+            "  public void testMethod2() {}",
+            "}");
+
+    PsiClass javaClass = ((PsiClassOwner) javaFile).getClasses()[0];
+    assertThat(javaClass).isNotNull();
+
+    ConfigurationContext context = createContextFromPsi(javaClass);
+    ConfigurationFromContext fromContext =
+        new BlazeJavaAbstractTestCaseConfigurationProducer()
+            .createConfigurationFromContext(context);
+    assertThat(fromContext).isNull();
+  }
+
+  @Test
+  public void testConfigurationCreatedFromAbstractClass() {
+    workspace.createPsiDirectory(new WorkspacePath("java/com/google/test"));
+    PsiFile abstractClassFile =
+        createAndIndexFile(
+            new WorkspacePath("java/com/google/test/AbstractTestCase.java"),
+            "package com.google.test;",
+            "public abstract class AbstractTestCase {}");
+
+    createAndIndexFile(
+        new WorkspacePath("java/com/google/test/TestClass.java"),
+        "package com.google.test;",
+        "import com.google.test.AbstractTestCase;",
+        "@org.junit.runner.RunWith(org.junit.runners.JUnit4.class)",
+        "public class TestClass extends AbstractTestCase {",
+        "  @org.junit.Test",
+        "  public void testMethod1() {}",
+        "  @org.junit.Test",
+        "  public void testMethod2() {}",
+        "}");
+
+    PsiClass javaClass = ((PsiClassOwner) abstractClassFile).getClasses()[0];
+    assertThat(javaClass).isNotNull();
+
+    ConfigurationContext context = createContextFromPsi(abstractClassFile);
+    List<ConfigurationFromContext> configurations = context.getConfigurationsFromContext();
+    assertThat(configurations).hasSize(1);
+
+    ConfigurationFromContext fromContext = configurations.get(0);
+    assertThat(fromContext.isProducedBy(BlazeJavaAbstractTestCaseConfigurationProducer.class))
+        .isTrue();
+    assertThat(fromContext.getSourceElement()).isEqualTo(javaClass);
+
+    RunConfiguration config = fromContext.getConfiguration();
+    assertThat(config).isInstanceOf(BlazeCommandRunConfiguration.class);
+    BlazeCommandRunConfiguration blazeConfig = (BlazeCommandRunConfiguration) config;
+    assertThat(blazeConfig.getTarget()).isNull();
+    assertThat(blazeConfig.getName()).isEqualTo("AbstractTestCase");
+
+    MockBlazeProjectDataBuilder builder = MockBlazeProjectDataBuilder.builder(workspaceRoot);
+    builder.setTargetMap(
+        TargetMapBuilder.builder()
+            .addTarget(
+                TargetIdeInfo.builder()
+                    .setKind("java_test")
+                    .setLabel("//java/com/google/test:TestClass")
+                    .addSource(sourceRoot("java/com/google/test/TestClass.java"))
+                    .build())
+            .build());
+    registerProjectService(
+        BlazeProjectDataManager.class, new MockBlazeProjectDataManager(builder.build()));
+
+    BlazeJavaAbstractTestCaseConfigurationProducer.chooseSubclass(
+        fromContext, context, EmptyRunnable.INSTANCE);
+
+    assertThat(blazeConfig.getTarget())
+        .isEqualTo(TargetExpression.fromString("//java/com/google/test:TestClass"));
+    assertThat(getTestFilterContents(blazeConfig))
+        .isEqualTo(BlazeFlags.TEST_FILTER + "=com.google.test.TestClass#");
+  }
+
+  @Test
+  public void testConfigurationCreatedFromMethodInAbstractClass() {
+    PsiFile abstractClassFile =
+        createAndIndexFile(
+            new WorkspacePath("java/com/google/test/AbstractTestCase.java"),
+            "package com.google.test;",
+            "public abstract class AbstractTestCase {",
+            "  @org.junit.Test",
+            "  public void testMethod() {}",
+            "}");
+
+    createAndIndexFile(
+        new WorkspacePath("java/com/google/test/TestClass.java"),
+        "package com.google.test;",
+        "import com.google.test.AbstractTestCase;",
+        "import org.junit.runner.RunWith;",
+        "import org.junit.runners.JUnit4;",
+        "@org.junit.runner.RunWith(org.junit.runners.JUnit4.class)",
+        "public class TestClass extends AbstractTestCase {}");
+
+    PsiClass javaClass = ((PsiClassOwner) abstractClassFile).getClasses()[0];
+    PsiMethod method = PsiUtils.findFirstChildOfClassRecursive(javaClass, PsiMethod.class);
+    assertThat(method).isNotNull();
+
+    ConfigurationContext context = createContextFromPsi(method);
+    List<ConfigurationFromContext> configurations = context.getConfigurationsFromContext();
+    assertThat(configurations).hasSize(1);
+
+    ConfigurationFromContext fromContext = configurations.get(0);
+    assertThat(fromContext.isProducedBy(BlazeJavaAbstractTestCaseConfigurationProducer.class))
+        .isTrue();
+    assertThat(fromContext.getSourceElement()).isEqualTo(method);
+
+    RunConfiguration config = fromContext.getConfiguration();
+    assertThat(config).isInstanceOf(BlazeCommandRunConfiguration.class);
+    BlazeCommandRunConfiguration blazeConfig = (BlazeCommandRunConfiguration) config;
+    assertThat(blazeConfig.getTarget()).isNull();
+    assertThat(blazeConfig.getName()).isEqualTo("AbstractTestCase.testMethod");
+
+    MockBlazeProjectDataBuilder builder = MockBlazeProjectDataBuilder.builder(workspaceRoot);
+    builder.setTargetMap(
+        TargetMapBuilder.builder()
+            .addTarget(
+                TargetIdeInfo.builder()
+                    .setKind("java_test")
+                    .setLabel("//java/com/google/test:TestClass")
+                    .addSource(sourceRoot("java/com/google/test/TestClass.java"))
+                    .build())
+            .build());
+    registerProjectService(
+        BlazeProjectDataManager.class, new MockBlazeProjectDataManager(builder.build()));
+
+    BlazeJavaAbstractTestCaseConfigurationProducer.chooseSubclass(
+        fromContext, context, EmptyRunnable.INSTANCE);
+
+    assertThat(blazeConfig.getTarget())
+        .isEqualTo(TargetExpression.fromString("//java/com/google/test:TestClass"));
+    assertThat(getTestFilterContents(blazeConfig))
+        .isEqualTo(BlazeFlags.TEST_FILTER + "=com.google.test.TestClass#testMethod$");
+  }
+}
diff --git a/java/tests/integrationtests/com/google/idea/blaze/java/run/producers/BlazeJavaMainClassConfigurationProducerTest.java b/java/tests/integrationtests/com/google/idea/blaze/java/run/producers/BlazeJavaMainClassConfigurationProducerTest.java
index 6135b2f..87c8f28 100644
--- a/java/tests/integrationtests/com/google/idea/blaze/java/run/producers/BlazeJavaMainClassConfigurationProducerTest.java
+++ b/java/tests/integrationtests/com/google/idea/blaze/java/run/producers/BlazeJavaMainClassConfigurationProducerTest.java
@@ -17,64 +17,31 @@
 
 import static com.google.common.truth.Truth.assertThat;
 
-import com.google.common.collect.ImmutableList;
-import com.google.idea.blaze.base.BlazeIntegrationTestCase;
-import com.google.idea.blaze.base.EditorTestHelper;
-import com.google.idea.blaze.base.ideinfo.ArtifactLocation;
 import com.google.idea.blaze.base.ideinfo.JavaIdeInfo;
 import com.google.idea.blaze.base.ideinfo.TargetIdeInfo;
-import com.google.idea.blaze.base.ideinfo.TargetMap;
 import com.google.idea.blaze.base.ideinfo.TargetMapBuilder;
 import com.google.idea.blaze.base.model.MockBlazeProjectDataBuilder;
 import com.google.idea.blaze.base.model.MockBlazeProjectDataManager;
-import com.google.idea.blaze.base.model.primitives.ExecutionRootPath;
 import com.google.idea.blaze.base.model.primitives.TargetExpression;
 import com.google.idea.blaze.base.model.primitives.WorkspacePath;
 import com.google.idea.blaze.base.run.BlazeRunConfiguration;
-import com.google.idea.blaze.base.sync.SyncCache;
+import com.google.idea.blaze.base.run.producer.BlazeRunConfigurationProducerTestCase;
 import com.google.idea.blaze.base.sync.data.BlazeProjectDataManager;
-import com.google.idea.blaze.base.sync.workspace.BlazeRoots;
-import com.intellij.execution.Location;
-import com.intellij.execution.PsiLocation;
-import com.intellij.execution.RunnerAndConfigurationSettings;
-import com.intellij.execution.actions.ConfigurationContext;
 import com.intellij.execution.configurations.RunConfiguration;
-import com.intellij.openapi.actionSystem.CommonDataKeys;
-import com.intellij.openapi.actionSystem.LangDataKeys;
-import com.intellij.openapi.module.ModuleUtil;
-import com.intellij.openapi.vfs.VirtualFile;
 import com.intellij.psi.PsiFile;
-import com.intellij.testFramework.MapDataContext;
-import java.io.File;
-import org.jetbrains.annotations.Nullable;
-import org.junit.After;
-import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.junit.runners.JUnit4;
 
-/** Integration tests for {@link BlazeJavaMainClassConfigurationProducer}. */
+/** Integration tests for {@link BlazeJavaMainClassRunConfigurationProducer}. */
 @RunWith(JUnit4.class)
-public class BlazeJavaMainClassConfigurationProducerTest extends BlazeIntegrationTestCase {
-
-  private EditorTestHelper editorTest;
-
-  @Before
-  public final void doSetup() {
-    BlazeProjectDataManager mockProjectDataManager =
-        new MockBlazeProjectDataManager(getMockBlazeProjectDataBuilder().build());
-    registerProjectService(BlazeProjectDataManager.class, mockProjectDataManager);
-    editorTest = new EditorTestHelper(getProject(), testFixture);
-  }
-
-  @After
-  public final void doTearDown() {
-    SyncCache.getInstance(getProject()).clear();
-  }
+public class BlazeJavaMainClassConfigurationProducerTest
+    extends BlazeRunConfigurationProducerTestCase {
 
   @Test
   public void testUniqueJavaBinaryChosen() {
-    setTargets(
+    MockBlazeProjectDataBuilder builder = MockBlazeProjectDataBuilder.builder(workspaceRoot);
+    builder.setTargetMap(
         TargetMapBuilder.builder()
             .addTarget(
                 TargetIdeInfo.builder()
@@ -83,14 +50,16 @@
                     .addSource(sourceRoot("com/google/binary/MainClass.java"))
                     .build())
             .build());
+    registerProjectService(
+        BlazeProjectDataManager.class, new MockBlazeProjectDataManager(builder.build()));
 
     PsiFile javaClass =
-        workspace.createPsiFile(
+        createAndIndexFile(
             WorkspacePath.createIfValid("com/google/binary/MainClass.java"),
             "package com.google.binary;",
             "import java.lang.String;",
             "public class MainClass {",
-            "  public static void main(String[] args) {}",
+            "  public static void main(java.lang.String[] args) {}",
             "}");
 
     RunConfiguration config = createConfigurationFromLocation(javaClass);
@@ -103,7 +72,8 @@
 
   @Test
   public void testNoJavaBinaryChosenIfNotInRDeps() {
-    setTargets(
+    MockBlazeProjectDataBuilder builder = MockBlazeProjectDataBuilder.builder(workspaceRoot);
+    builder.setTargetMap(
         TargetMapBuilder.builder()
             .addTarget(
                 TargetIdeInfo.builder()
@@ -112,14 +82,16 @@
                     .addSource(sourceRoot("com/google/binary/OtherClass.java"))
                     .build())
             .build());
+    registerProjectService(
+        BlazeProjectDataManager.class, new MockBlazeProjectDataManager(builder.build()));
 
     PsiFile javaClass =
-        workspace.createPsiFile(
+        createAndIndexFile(
             WorkspacePath.createIfValid("com/google/binary/MainClass.java"),
             "package com.google.binary;",
             "import java.lang.String;",
             "public class MainClass {",
-            "  public static void main(String[] args) {}",
+            "  public static void main(java.lang.String[] args) {}",
             "}");
 
     assertThat(createConfigurationFromLocation(javaClass))
@@ -128,7 +100,8 @@
 
   @Test
   public void testNoResultForClassWithoutMainMethod() {
-    setTargets(
+    MockBlazeProjectDataBuilder builder = MockBlazeProjectDataBuilder.builder(workspaceRoot);
+    builder.setTargetMap(
         TargetMapBuilder.builder()
             .addTarget(
                 TargetIdeInfo.builder()
@@ -138,9 +111,11 @@
                     .setJavaInfo(JavaIdeInfo.builder().setMainClass("com.google.binary.MainClass"))
                     .build())
             .build());
+    registerProjectService(
+        BlazeProjectDataManager.class, new MockBlazeProjectDataManager(builder.build()));
 
     PsiFile javaClass =
-        workspace.createPsiFile(
+        createAndIndexFile(
             WorkspacePath.createIfValid("com/google/binary/MainClass.java"),
             "package com.google.binary;",
             "public class MainClass {}");
@@ -150,7 +125,8 @@
 
   @Test
   public void testJavaBinaryWithMatchingNameChosen() {
-    setTargets(
+    MockBlazeProjectDataBuilder builder = MockBlazeProjectDataBuilder.builder(workspaceRoot);
+    builder.setTargetMap(
         TargetMapBuilder.builder()
             .addTarget(
                 TargetIdeInfo.builder()
@@ -165,14 +141,16 @@
                     .addSource(sourceRoot("com/google/binary/MainClass.java"))
                     .build())
             .build());
+    registerProjectService(
+        BlazeProjectDataManager.class, new MockBlazeProjectDataManager(builder.build()));
 
     PsiFile javaClass =
-        workspace.createPsiFile(
+        createAndIndexFile(
             WorkspacePath.createIfValid("com/google/binary/MainClass.java"),
             "package com.google.binary;",
             "import java.lang.String;",
             "public class MainClass {",
-            "  public static void main(String[] args) {}",
+            "  public static void main(java.lang.String[] args) {}",
             "}");
 
     RunConfiguration config = createConfigurationFromLocation(javaClass);
@@ -184,7 +162,8 @@
 
   @Test
   public void testJavaBinaryWithMatchingMainClassChosen() {
-    setTargets(
+    MockBlazeProjectDataBuilder builder = MockBlazeProjectDataBuilder.builder(workspaceRoot);
+    builder.setTargetMap(
         TargetMapBuilder.builder()
             .addTarget(
                 TargetIdeInfo.builder()
@@ -200,14 +179,16 @@
                     .addSource(sourceRoot("com/google/binary/MainClass.java"))
                     .build())
             .build());
+    registerProjectService(
+        BlazeProjectDataManager.class, new MockBlazeProjectDataManager(builder.build()));
 
     PsiFile javaClass =
-        workspace.createPsiFile(
+        createAndIndexFile(
             WorkspacePath.createIfValid("com/google/binary/MainClass.java"),
             "package com.google.binary;",
             "import java.lang.String;",
             "public class MainClass {",
-            "  public static void main(String[] args) {}",
+            "  public static void main(java.lang.String[] args) {}",
             "}");
 
     RunConfiguration config = createConfigurationFromLocation(javaClass);
@@ -217,48 +198,4 @@
     assertThat(blazeConfig.getTarget())
         .isEqualTo(TargetExpression.fromString("//com/google/binary:OtherName"));
   }
-
-  @Nullable
-  private RunConfiguration createConfigurationFromLocation(PsiFile psiFile) {
-    // a nauseating hack to force IntelliJ to recognize 'main' methods...
-    workspace.createPsiFile(
-        WorkspacePath.createIfValid("java/lang/String.java"),
-        "package java.lang;",
-        "public class String {}");
-    editorTest.openFileInEditor(psiFile);
-
-    final MapDataContext dataContext = new MapDataContext();
-
-    dataContext.put(CommonDataKeys.PROJECT, getProject());
-    dataContext.put(LangDataKeys.MODULE, ModuleUtil.findModuleForPsiElement(psiFile));
-    dataContext.put(Location.DATA_KEY, PsiLocation.fromPsiElement(psiFile));
-    RunnerAndConfigurationSettings settings =
-        ConfigurationContext.getFromContext(dataContext).getConfiguration();
-    return settings != null ? settings.getConfiguration() : null;
-  }
-
-  private MockBlazeProjectDataBuilder getMockBlazeProjectDataBuilder() {
-    String executionRootPath = "usr/local/_blaze_";
-    VirtualFile vf = fileSystem.createDirectory(executionRootPath);
-    BlazeRoots fakeRoots =
-        new BlazeRoots(
-            new File(vf.getPath()),
-            ImmutableList.of(workspaceRoot.directory()),
-            new ExecutionRootPath("out/crosstool/bin"),
-            new ExecutionRootPath("out/crosstool/gen"),
-            null);
-    return MockBlazeProjectDataBuilder.builder(workspaceRoot).setBlazeRoots(fakeRoots);
-  }
-
-  private void setTargets(TargetMap targets) {
-    BlazeProjectDataManager mockProjectDataManager =
-        new MockBlazeProjectDataManager(
-            getMockBlazeProjectDataBuilder().setTargetMap(targets).build());
-    registerProjectService(BlazeProjectDataManager.class, mockProjectDataManager);
-    SyncCache.getInstance(getProject()).clear();
-  }
-
-  private static ArtifactLocation sourceRoot(String relativePath) {
-    return ArtifactLocation.builder().setRelativePath(relativePath).setIsSource(true).build();
-  }
 }
diff --git a/java/tests/integrationtests/com/google/idea/blaze/java/run/producers/BlazeJavaTestClassConfigurationProducerTest.java b/java/tests/integrationtests/com/google/idea/blaze/java/run/producers/BlazeJavaTestClassConfigurationProducerTest.java
new file mode 100644
index 0000000..ecf15d1
--- /dev/null
+++ b/java/tests/integrationtests/com/google/idea/blaze/java/run/producers/BlazeJavaTestClassConfigurationProducerTest.java
@@ -0,0 +1,135 @@
+/*
+ * Copyright 2017 The Bazel Authors. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.idea.blaze.java.run.producers;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.google.idea.blaze.base.command.BlazeCommandName;
+import com.google.idea.blaze.base.ideinfo.TargetIdeInfo;
+import com.google.idea.blaze.base.ideinfo.TargetMapBuilder;
+import com.google.idea.blaze.base.model.MockBlazeProjectDataBuilder;
+import com.google.idea.blaze.base.model.MockBlazeProjectDataManager;
+import com.google.idea.blaze.base.model.primitives.TargetExpression;
+import com.google.idea.blaze.base.model.primitives.WorkspacePath;
+import com.google.idea.blaze.base.run.BlazeCommandRunConfiguration;
+import com.google.idea.blaze.base.run.producer.BlazeRunConfigurationProducerTestCase;
+import com.google.idea.blaze.base.sync.data.BlazeProjectDataManager;
+import com.intellij.execution.actions.ConfigurationContext;
+import com.intellij.execution.actions.ConfigurationFromContext;
+import com.intellij.psi.PsiClass;
+import com.intellij.psi.PsiClassOwner;
+import com.intellij.psi.PsiFile;
+import java.util.List;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** Integration tests for {@link BlazeJavaTestClassConfigurationProducer}. */
+@RunWith(JUnit4.class)
+public class BlazeJavaTestClassConfigurationProducerTest
+    extends BlazeRunConfigurationProducerTestCase {
+
+  @Test
+  public void testProducedFromPsiFile() {
+    PsiFile javaFile =
+        createAndIndexFile(
+            new WorkspacePath("java/com/google/test/TestClass.java"),
+            "package com.google.test;",
+            "@org.junit.runner.RunWith(org.junit.runners.JUnit4.class)",
+            "public class TestClass {",
+            "  @org.junit.Test",
+            "  public void testMethod1() {}",
+            "  @org.junit.Test",
+            "  public void testMethod2() {}",
+            "}");
+
+    MockBlazeProjectDataBuilder builder = MockBlazeProjectDataBuilder.builder(workspaceRoot);
+    builder.setTargetMap(
+        TargetMapBuilder.builder()
+            .addTarget(
+                TargetIdeInfo.builder()
+                    .setKind("java_test")
+                    .setLabel("//java/com/google/test:TestClass")
+                    .addSource(sourceRoot("java/com/google/test/TestClass.java"))
+                    .build())
+            .build());
+    registerProjectService(
+        BlazeProjectDataManager.class, new MockBlazeProjectDataManager(builder.build()));
+
+    ConfigurationContext context = createContextFromPsi(javaFile);
+    List<ConfigurationFromContext> configurations = context.getConfigurationsFromContext();
+    assertThat(configurations).hasSize(1);
+
+    ConfigurationFromContext fromContext = configurations.get(0);
+    assertThat(fromContext.isProducedBy(BlazeJavaTestClassConfigurationProducer.class)).isTrue();
+    assertThat(fromContext.getConfiguration()).isInstanceOf(BlazeCommandRunConfiguration.class);
+
+    BlazeCommandRunConfiguration config =
+        (BlazeCommandRunConfiguration) fromContext.getConfiguration();
+    assertThat(config.getTarget())
+        .isEqualTo(TargetExpression.fromString("//java/com/google/test:TestClass"));
+    assertThat(getTestFilterContents(config)).isEqualTo("--test_filter=com.google.test.TestClass#");
+    assertThat(config.getName()).isEqualTo("Blaze test TestClass");
+    assertThat(getCommandType(config)).isEqualTo(BlazeCommandName.TEST);
+  }
+
+  @Test
+  public void testProducedFromPsiClass() {
+    PsiFile javaFile =
+        createAndIndexFile(
+            new WorkspacePath("java/com/google/test/TestClass.java"),
+            "package com.google.test;",
+            "@org.junit.runner.RunWith(org.junit.runners.JUnit4.class)",
+            "public class TestClass {",
+            "  @org.junit.Test",
+            "  public void testMethod1() {}",
+            "  @org.junit.Test",
+            "  public void testMethod2() {}",
+            "}");
+
+    MockBlazeProjectDataBuilder builder = MockBlazeProjectDataBuilder.builder(workspaceRoot);
+    builder.setTargetMap(
+        TargetMapBuilder.builder()
+            .addTarget(
+                TargetIdeInfo.builder()
+                    .setKind("java_test")
+                    .setLabel("//java/com/google/test:TestClass")
+                    .addSource(sourceRoot("java/com/google/test/TestClass.java"))
+                    .build())
+            .build());
+    registerProjectService(
+        BlazeProjectDataManager.class, new MockBlazeProjectDataManager(builder.build()));
+
+    PsiClass javaClass = ((PsiClassOwner) javaFile).getClasses()[0];
+    assertThat(javaClass).isNotNull();
+
+    ConfigurationContext context = createContextFromPsi(javaClass);
+    List<ConfigurationFromContext> configurations = context.getConfigurationsFromContext();
+    assertThat(configurations).hasSize(1);
+
+    ConfigurationFromContext fromContext = configurations.get(0);
+    assertThat(fromContext.isProducedBy(BlazeJavaTestClassConfigurationProducer.class)).isTrue();
+    assertThat(fromContext.getConfiguration()).isInstanceOf(BlazeCommandRunConfiguration.class);
+
+    BlazeCommandRunConfiguration config =
+        (BlazeCommandRunConfiguration) fromContext.getConfiguration();
+    assertThat(config.getTarget())
+        .isEqualTo(TargetExpression.fromString("//java/com/google/test:TestClass"));
+    assertThat(getTestFilterContents(config)).isEqualTo("--test_filter=com.google.test.TestClass#");
+    assertThat(config.getName()).isEqualTo("Blaze test TestClass");
+    assertThat(getCommandType(config)).isEqualTo(BlazeCommandName.TEST);
+  }
+}
diff --git a/java/tests/integrationtests/com/google/idea/blaze/java/run/producers/BlazeJavaTestMethodConfigurationProducerTest.java b/java/tests/integrationtests/com/google/idea/blaze/java/run/producers/BlazeJavaTestMethodConfigurationProducerTest.java
new file mode 100644
index 0000000..bd8118e
--- /dev/null
+++ b/java/tests/integrationtests/com/google/idea/blaze/java/run/producers/BlazeJavaTestMethodConfigurationProducerTest.java
@@ -0,0 +1,90 @@
+/*
+ * Copyright 2017 The Bazel Authors. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.idea.blaze.java.run.producers;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.google.idea.blaze.base.command.BlazeCommandName;
+import com.google.idea.blaze.base.ideinfo.TargetIdeInfo;
+import com.google.idea.blaze.base.ideinfo.TargetMapBuilder;
+import com.google.idea.blaze.base.lang.buildfile.psi.util.PsiUtils;
+import com.google.idea.blaze.base.model.MockBlazeProjectDataBuilder;
+import com.google.idea.blaze.base.model.MockBlazeProjectDataManager;
+import com.google.idea.blaze.base.model.primitives.TargetExpression;
+import com.google.idea.blaze.base.model.primitives.WorkspacePath;
+import com.google.idea.blaze.base.run.BlazeCommandRunConfiguration;
+import com.google.idea.blaze.base.run.producer.BlazeRunConfigurationProducerTestCase;
+import com.google.idea.blaze.base.sync.data.BlazeProjectDataManager;
+import com.intellij.execution.actions.ConfigurationContext;
+import com.intellij.execution.actions.ConfigurationFromContext;
+import com.intellij.psi.PsiFile;
+import com.intellij.psi.PsiMethod;
+import java.util.List;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** Integration tests for {@link BlazeJavaTestMethodConfigurationProducer}. */
+@RunWith(JUnit4.class)
+public class BlazeJavaTestMethodConfigurationProducerTest
+    extends BlazeRunConfigurationProducerTestCase {
+
+  @Test
+  public void testProducedFromPsiMethod() {
+    PsiFile javaFile =
+        createAndIndexFile(
+            new WorkspacePath("java/com/google/test/TestClass.java"),
+            "package com.google.test;",
+            "@org.junit.runner.RunWith(org.junit.runners.JUnit4.class)",
+            "public class TestClass {",
+            "  @org.junit.Test",
+            "  public void testMethod1() {}",
+            "}");
+
+    MockBlazeProjectDataBuilder builder = MockBlazeProjectDataBuilder.builder(workspaceRoot);
+    builder.setTargetMap(
+        TargetMapBuilder.builder()
+            .addTarget(
+                TargetIdeInfo.builder()
+                    .setKind("java_test")
+                    .setLabel("//java/com/google/test:TestClass")
+                    .addSource(sourceRoot("java/com/google/test/TestClass.java"))
+                    .build())
+            .build());
+    registerProjectService(
+        BlazeProjectDataManager.class, new MockBlazeProjectDataManager(builder.build()));
+
+    PsiMethod method = PsiUtils.findFirstChildOfClassRecursive(javaFile, PsiMethod.class);
+    assertThat(method).isNotNull();
+
+    ConfigurationContext context = createContextFromPsi(method);
+    List<ConfigurationFromContext> configurations = context.getConfigurationsFromContext();
+    assertThat(configurations).hasSize(1);
+
+    ConfigurationFromContext fromContext = configurations.get(0);
+    assertThat(fromContext.isProducedBy(BlazeJavaTestMethodConfigurationProducer.class)).isTrue();
+    assertThat(fromContext.getConfiguration()).isInstanceOf(BlazeCommandRunConfiguration.class);
+
+    BlazeCommandRunConfiguration config =
+        (BlazeCommandRunConfiguration) fromContext.getConfiguration();
+    assertThat(config.getTarget())
+        .isEqualTo(TargetExpression.fromString("//java/com/google/test:TestClass"));
+    assertThat(getTestFilterContents(config))
+        .isEqualTo("--test_filter=com.google.test.TestClass#testMethod1$");
+    assertThat(config.getName()).isEqualTo("Blaze test TestClass.testMethod1");
+    assertThat(getCommandType(config)).isEqualTo(BlazeCommandName.TEST);
+  }
+}
diff --git a/java/tests/integrationtests/com/google/idea/blaze/java/run/producers/MultipleJavaClassesTestConfigurationProducerTest.java b/java/tests/integrationtests/com/google/idea/blaze/java/run/producers/MultipleJavaClassesTestConfigurationProducerTest.java
new file mode 100644
index 0000000..731743a
--- /dev/null
+++ b/java/tests/integrationtests/com/google/idea/blaze/java/run/producers/MultipleJavaClassesTestConfigurationProducerTest.java
@@ -0,0 +1,409 @@
+/*
+ * Copyright 2017 The Bazel Authors. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.idea.blaze.java.run.producers;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.google.idea.blaze.base.command.BlazeCommandName;
+import com.google.idea.blaze.base.ideinfo.TargetIdeInfo;
+import com.google.idea.blaze.base.ideinfo.TargetMapBuilder;
+import com.google.idea.blaze.base.model.MockBlazeProjectDataBuilder;
+import com.google.idea.blaze.base.model.MockBlazeProjectDataManager;
+import com.google.idea.blaze.base.model.primitives.TargetExpression;
+import com.google.idea.blaze.base.model.primitives.WorkspacePath;
+import com.google.idea.blaze.base.run.BlazeCommandRunConfiguration;
+import com.google.idea.blaze.base.run.producer.BlazeRunConfigurationProducerTestCase;
+import com.google.idea.blaze.base.sync.data.BlazeProjectDataManager;
+import com.intellij.execution.actions.ConfigurationContext;
+import com.intellij.execution.actions.ConfigurationFromContext;
+import com.intellij.openapi.application.ApplicationManager;
+import com.intellij.openapi.roots.ContentEntry;
+import com.intellij.openapi.roots.ModifiableRootModel;
+import com.intellij.openapi.roots.ModuleRootManager;
+import com.intellij.openapi.roots.SourceFolder;
+import com.intellij.openapi.vfs.VirtualFile;
+import com.intellij.psi.PsiDirectory;
+import com.intellij.psi.PsiElement;
+import com.intellij.psi.PsiFile;
+import java.util.List;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** Integration tests for {@link MultipleJavaClassesTestConfigurationProducer}. */
+@RunWith(JUnit4.class)
+public class MultipleJavaClassesTestConfigurationProducerTest
+    extends BlazeRunConfigurationProducerTestCase {
+
+  private SourceFolder javaSourceRoot;
+
+  @Before
+  public final void addSourceFolder() {
+    // create a source root so that the package prefixes are correct.
+    VirtualFile pkgRoot = workspace.createDirectory(new WorkspacePath("java"));
+    ApplicationManager.getApplication()
+        .runWriteAction(
+            () -> {
+              final ModifiableRootModel model =
+                  ModuleRootManager.getInstance(testFixture.getModule()).getModifiableModel();
+              ContentEntry contentEntry = model.getContentEntries()[0];
+              javaSourceRoot = contentEntry.addSourceFolder(pkgRoot, false, "");
+              model.commit();
+            });
+
+    BlazeProjectDataManager mockProjectDataManager =
+        new MockBlazeProjectDataManager(MockBlazeProjectDataBuilder.builder(workspaceRoot).build());
+    registerProjectService(BlazeProjectDataManager.class, mockProjectDataManager);
+  }
+
+  @After
+  public final void removeSourceFolder() {
+    ApplicationManager.getApplication()
+        .runWriteAction(
+            () -> {
+              final ModifiableRootModel model =
+                  ModuleRootManager.getInstance(testFixture.getModule()).getModifiableModel();
+              ContentEntry contentEntry = model.getContentEntries()[0];
+              contentEntry.removeSourceFolder(javaSourceRoot);
+              model.commit();
+            });
+  }
+
+  @Test
+  public void testProducedFromDirectory() {
+    MockBlazeProjectDataBuilder builder = MockBlazeProjectDataBuilder.builder(workspaceRoot);
+    builder.setTargetMap(
+        TargetMapBuilder.builder()
+            .addTarget(
+                TargetIdeInfo.builder()
+                    .setKind("java_test")
+                    .setLabel("//java/com/google/test:TestClass")
+                    .addSource(sourceRoot("java/com/google/test/TestClass.java"))
+                    .build())
+            .build());
+    registerProjectService(
+        BlazeProjectDataManager.class, new MockBlazeProjectDataManager(builder.build()));
+
+    PsiDirectory directory =
+        workspace.createPsiDirectory(new WorkspacePath("java/com/google/test"));
+    createAndIndexFile(
+        new WorkspacePath("java/com/google/test/TestClass.java"),
+        "package com.google.test;",
+        "@org.junit.runner.RunWith(org.junit.runners.JUnit4.class)",
+        "public class TestClass {",
+        "  @org.junit.Test",
+        "  public void testMethod() {}",
+        "}");
+
+    ConfigurationContext context = createContextFromPsi(directory);
+    List<ConfigurationFromContext> configurations = context.getConfigurationsFromContext();
+    assertThat(configurations).hasSize(1);
+
+    ConfigurationFromContext fromContext = configurations.get(0);
+    assertThat(fromContext.isProducedBy(MultipleJavaClassesTestConfigurationProducer.class))
+        .isTrue();
+    assertThat(fromContext.getConfiguration()).isInstanceOf(BlazeCommandRunConfiguration.class);
+
+    BlazeCommandRunConfiguration config =
+        (BlazeCommandRunConfiguration) fromContext.getConfiguration();
+    assertThat(config.getTarget())
+        .isEqualTo(TargetExpression.fromString("//java/com/google/test:TestClass"));
+    assertThat(getTestFilterContents(config)).isEqualTo("--test_filter=com.google.test");
+    assertThat(config.getName()).isEqualTo("Blaze test all in directory 'test'");
+    assertThat(getCommandType(config)).isEqualTo(BlazeCommandName.TEST);
+  }
+
+  @Test
+  public void testProducedFromDirectoryWithNestedTests() {
+    MockBlazeProjectDataBuilder builder = MockBlazeProjectDataBuilder.builder(workspaceRoot);
+    builder.setTargetMap(
+        TargetMapBuilder.builder()
+            .addTarget(
+                TargetIdeInfo.builder()
+                    .setKind("java_test")
+                    .setLabel("//java/com/google/test:TestClass")
+                    .addSource(sourceRoot("java/com/google/test/TestClass.java"))
+                    .build())
+            .build());
+    registerProjectService(
+        BlazeProjectDataManager.class, new MockBlazeProjectDataManager(builder.build()));
+
+    PsiDirectory directory = workspace.createPsiDirectory(new WorkspacePath("java/com/google"));
+    createAndIndexFile(
+        new WorkspacePath("java/com/google/test/TestClass.java"),
+        "package com.google.test;",
+        "@org.junit.runner.RunWith(org.junit.runners.JUnit4.class)",
+        "public class TestClass {",
+        "  @org.junit.Test",
+        "  public void testMethod() {}",
+        "}");
+
+    ConfigurationContext context = createContextFromPsi(directory);
+    List<ConfigurationFromContext> configurations = context.getConfigurationsFromContext();
+    assertThat(configurations).hasSize(1);
+
+    ConfigurationFromContext fromContext = configurations.get(0);
+    assertThat(fromContext.isProducedBy(MultipleJavaClassesTestConfigurationProducer.class))
+        .isTrue();
+    assertThat(fromContext.getConfiguration()).isInstanceOf(BlazeCommandRunConfiguration.class);
+
+    BlazeCommandRunConfiguration config =
+        (BlazeCommandRunConfiguration) fromContext.getConfiguration();
+    assertThat(config.getTarget())
+        .isEqualTo(TargetExpression.fromString("//java/com/google/test:TestClass"));
+    assertThat(getTestFilterContents(config)).isEqualTo("--test_filter=com.google");
+    assertThat(config.getName()).isEqualTo("Blaze test all in directory 'google'");
+    assertThat(getCommandType(config)).isEqualTo(BlazeCommandName.TEST);
+  }
+
+  @Test
+  public void testNoFilterIfDirectoryAtPackageRoot() {
+    MockBlazeProjectDataBuilder builder = MockBlazeProjectDataBuilder.builder(workspaceRoot);
+    builder.setTargetMap(
+        TargetMapBuilder.builder()
+            .addTarget(
+                TargetIdeInfo.builder()
+                    .setKind("java_test")
+                    .setLabel("//java/com/google/test:TestClass")
+                    .addSource(sourceRoot("java/com/google/test/TestClass.java"))
+                    .build())
+            .build());
+    registerProjectService(
+        BlazeProjectDataManager.class, new MockBlazeProjectDataManager(builder.build()));
+
+    PsiDirectory directory = workspace.createPsiDirectory(new WorkspacePath("java"));
+    createAndIndexFile(
+        new WorkspacePath("java/com/google/test/TestClass.java"),
+        "package com.google.test;",
+        "@org.junit.runner.RunWith(org.junit.runners.JUnit4.class)",
+        "public class TestClass {",
+        "  @org.junit.Test",
+        "  public void testMethod() {}",
+        "}");
+
+    ConfigurationContext context = createContextFromPsi(directory);
+    List<ConfigurationFromContext> configurations = context.getConfigurationsFromContext();
+    assertThat(configurations).hasSize(1);
+
+    ConfigurationFromContext fromContext = configurations.get(0);
+    assertThat(fromContext.isProducedBy(MultipleJavaClassesTestConfigurationProducer.class))
+        .isTrue();
+    assertThat(fromContext.getConfiguration()).isInstanceOf(BlazeCommandRunConfiguration.class);
+
+    BlazeCommandRunConfiguration config =
+        (BlazeCommandRunConfiguration) fromContext.getConfiguration();
+    assertThat(getTestFilterContents(config)).isNull();
+    assertThat(config.getName()).isEqualTo("Blaze test test:TestClass");
+    assertThat(getCommandType(config)).isEqualTo(BlazeCommandName.TEST);
+  }
+
+  @Test
+  public void testNotProducedFromDirectoryWithoutTests() {
+    MockBlazeProjectDataBuilder builder = MockBlazeProjectDataBuilder.builder(workspaceRoot);
+    builder.setTargetMap(
+        TargetMapBuilder.builder()
+            .addTarget(
+                TargetIdeInfo.builder()
+                    .setKind("java_test")
+                    .setLabel("//java/com/google/test:TestClass")
+                    .addSource(sourceRoot("java/com/google/test/TestClass.java"))
+                    .build())
+            .build());
+    registerProjectService(
+        BlazeProjectDataManager.class, new MockBlazeProjectDataManager(builder.build()));
+
+    PsiDirectory directory =
+        workspace.createPsiDirectory(new WorkspacePath("java/com/google/test"));
+
+    ConfigurationContext context = createContextFromPsi(directory);
+    assertThat(
+            new MultipleJavaClassesTestConfigurationProducer()
+                .createConfigurationFromContext(context))
+        .isNull();
+  }
+
+  @Test
+  public void testNotProducedFromSingleTestFile() {
+    MockBlazeProjectDataBuilder builder = MockBlazeProjectDataBuilder.builder(workspaceRoot);
+    builder.setTargetMap(
+        TargetMapBuilder.builder()
+            .addTarget(
+                TargetIdeInfo.builder()
+                    .setKind("java_test")
+                    .setLabel("//java/com/google/test:TestClass")
+                    .addSource(sourceRoot("java/com/google/test/TestClass.java"))
+                    .build())
+            .build());
+    registerProjectService(
+        BlazeProjectDataManager.class, new MockBlazeProjectDataManager(builder.build()));
+
+    workspace.createPsiDirectory(new WorkspacePath("java/com/google/test"));
+    PsiFile file =
+        createAndIndexFile(
+            new WorkspacePath("java/com/google/test/TestClass.java"),
+            "package com.google.test;",
+            "@org.junit.runner.RunWith(org.junit.runners.JUnit4.class)",
+            "public class TestClass {",
+            "  @org.junit.Test",
+            "  public void testMethod() {}",
+            "}");
+
+    ConfigurationContext context = createContextFromPsi(file);
+    assertThat(
+            new MultipleJavaClassesTestConfigurationProducer()
+                .createConfigurationFromContext(context))
+        .isNull();
+  }
+
+  @Test
+  public void testProducedFromTestFiles() {
+    MockBlazeProjectDataBuilder builder = MockBlazeProjectDataBuilder.builder(workspaceRoot);
+    builder.setTargetMap(
+        TargetMapBuilder.builder()
+            .addTarget(
+                TargetIdeInfo.builder()
+                    .setKind("java_test")
+                    .setLabel("//java/com/google/test:allTests")
+                    .addSource(sourceRoot("java/com/google/test/TestClass1.java"))
+                    .addSource(sourceRoot("java/com/google/test/TestClass2.java"))
+                    .build())
+            .build());
+    registerProjectService(
+        BlazeProjectDataManager.class, new MockBlazeProjectDataManager(builder.build()));
+
+    workspace.createPsiDirectory(new WorkspacePath("java/com/google/test"));
+    PsiFile testClass1 =
+        createAndIndexFile(
+            new WorkspacePath("java/com/google/test/TestClass1.java"),
+            "package com.google.test;",
+            "@org.junit.runner.RunWith(org.junit.runners.JUnit4.class)",
+            "public class TestClass1 {",
+            "  @org.junit.Test",
+            "  public void testMethod() {}",
+            "}");
+    PsiFile testClass2 =
+        createAndIndexFile(
+            new WorkspacePath("java/com/google/test/TestClass2.java"),
+            "package com.google.test;",
+            "@org.junit.runner.RunWith(org.junit.runners.JUnit4.class)",
+            "public class TestClass2 {",
+            "  @org.junit.Test",
+            "  public void testMethod() {}",
+            "}");
+
+    ConfigurationContext context =
+        createContextFromMultipleElements(new PsiElement[] {testClass1, testClass2});
+    ConfigurationFromContext fromContext =
+        new MultipleJavaClassesTestConfigurationProducer().createConfigurationFromContext(context);
+    assertThat(fromContext).isNotNull();
+    assertThat(fromContext.getConfiguration()).isInstanceOf(BlazeCommandRunConfiguration.class);
+
+    BlazeCommandRunConfiguration config =
+        (BlazeCommandRunConfiguration) fromContext.getConfiguration();
+    assertThat(config.getTarget())
+        .isEqualTo(TargetExpression.fromString("//java/com/google/test:allTests"));
+    assertThat(getTestFilterContents(config))
+        .isEqualTo("--test_filter=com.google.test.TestClass1#|com.google.test.TestClass2#");
+    assertThat(config.getName()).isEqualTo("Blaze test TestClass1 and 1 others");
+    assertThat(getCommandType(config)).isEqualTo(BlazeCommandName.TEST);
+  }
+
+  @Test
+  public void testNotProducedFromTestFilesInDifferentTestTargets() {
+    MockBlazeProjectDataBuilder builder = MockBlazeProjectDataBuilder.builder(workspaceRoot);
+    builder.setTargetMap(
+        TargetMapBuilder.builder()
+            .addTarget(
+                TargetIdeInfo.builder()
+                    .setKind("java_test")
+                    .setLabel("//java/com/google/test:TestClass1")
+                    .addSource(sourceRoot("java/com/google/test/TestClass1.java"))
+                    .build())
+            .addTarget(
+                TargetIdeInfo.builder()
+                    .setKind("java_test")
+                    .setLabel("//java/com/google/test:TestClass2")
+                    .addSource(sourceRoot("java/com/google/test/TestClass2.java"))
+                    .build())
+            .build());
+    registerProjectService(
+        BlazeProjectDataManager.class, new MockBlazeProjectDataManager(builder.build()));
+
+    workspace.createPsiDirectory(new WorkspacePath("java/com/google/test"));
+    PsiFile testClass1 =
+        createAndIndexFile(
+            new WorkspacePath("java/com/google/test/TestClass1.java"),
+            "package com.google.test;",
+            "@org.junit.runner.RunWith(org.junit.runners.JUnit4.class)",
+            "public class TestClass1 {",
+            "  @org.junit.Test",
+            "  public void testMethod() {}",
+            "}");
+    PsiFile testClass2 =
+        createAndIndexFile(
+            new WorkspacePath("java/com/google/test/TestClass2.java"),
+            "package com.google.test;",
+            "@org.junit.runner.RunWith(org.junit.runners.JUnit4.class)",
+            "public class TestClass2 {",
+            "  @org.junit.Test",
+            "  public void testMethod() {}",
+            "}");
+
+    ConfigurationContext context =
+        createContextFromMultipleElements(new PsiElement[] {testClass1, testClass2});
+    assertThat(
+            new MultipleJavaClassesTestConfigurationProducer()
+                .createConfigurationFromContext(context))
+        .isNull();
+  }
+
+  @Test
+  public void testNotProducedFromNonTestFiles() {
+    MockBlazeProjectDataBuilder builder = MockBlazeProjectDataBuilder.builder(workspaceRoot);
+    builder.setTargetMap(
+        TargetMapBuilder.builder()
+            .addTarget(
+                TargetIdeInfo.builder()
+                    .setKind("java_test")
+                    .setLabel("//java/com/google/test:allTests")
+                    .addSource(sourceRoot("java/com/google/test/NonTestClass1.java"))
+                    .addSource(sourceRoot("java/com/google/test/NonTestClass2.java"))
+                    .build())
+            .build());
+    registerProjectService(
+        BlazeProjectDataManager.class, new MockBlazeProjectDataManager(builder.build()));
+
+    workspace.createPsiDirectory(new WorkspacePath("java/com/google/test"));
+    PsiFile testClass1 =
+        createAndIndexFile(
+            new WorkspacePath("java/com/google/test/NonTestClass1.java"),
+            "package com.google.test;",
+            "public class NonTestClass1 {");
+    PsiFile testClass2 =
+        createAndIndexFile(
+            new WorkspacePath("java/com/google/test/NonTestClass2.java"),
+            "package com.google.test;",
+            "public class NonTestClass2 {}");
+
+    ConfigurationContext context =
+        createContextFromMultipleElements(new PsiElement[] {testClass1, testClass2});
+    ConfigurationFromContext fromContext =
+        new MultipleJavaClassesTestConfigurationProducer().createConfigurationFromContext(context);
+    assertThat(fromContext).isNull();
+  }
+}
diff --git a/java/tests/unittests/com/google/idea/blaze/java/run/BlazeJavaRunProfileStateTest.java b/java/tests/unittests/com/google/idea/blaze/java/run/BlazeJavaRunProfileStateTest.java
index 622105e..d31ef3f 100644
--- a/java/tests/unittests/com/google/idea/blaze/java/run/BlazeJavaRunProfileStateTest.java
+++ b/java/tests/unittests/com/google/idea/blaze/java/run/BlazeJavaRunProfileStateTest.java
@@ -16,10 +16,13 @@
 package com.google.idea.blaze.java.run;
 
 import static com.google.common.truth.Truth.assertThat;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
 
 import com.google.common.base.Predicate;
 import com.google.common.collect.ImmutableList;
 import com.google.idea.blaze.base.BlazeTestCase;
+import com.google.idea.blaze.base.bazel.BuildSystemProvider;
 import com.google.idea.blaze.base.command.BlazeCommandName;
 import com.google.idea.blaze.base.command.BlazeFlags;
 import com.google.idea.blaze.base.command.BuildFlagsProvider;
@@ -60,8 +63,7 @@
   @Override
   protected void initTest(
       @NotNull Container applicationServices, @NotNull Container projectServices) {
-    projectServices.register(
-        BlazeImportSettingsManager.class, new BlazeImportSettingsManager(project));
+    projectServices.register(BlazeImportSettingsManager.class, new BlazeImportSettingsManager());
     BlazeImportSettingsManager.getInstance(getProject()).setImportSettings(DUMMY_IMPORT_SETTINGS);
 
     ExperimentService experimentService = new MockExperimentService();
@@ -75,6 +77,11 @@
             BlazeCommandRunConfigurationHandlerProvider.EP_NAME,
             BlazeCommandRunConfigurationHandlerProvider.class);
     handlerProviderEp.registerExtension(new BlazeCommandGenericRunConfigurationHandlerProvider());
+    ExtensionPointImpl<BuildSystemProvider> buildSystemProviderExtensionPoint =
+        registerExtensionPoint(BuildSystemProvider.EP_NAME, BuildSystemProvider.class);
+    BuildSystemProvider buildSystemProvider = mock(BuildSystemProvider.class);
+    when(buildSystemProvider.getBinaryPath()).thenReturn("/usr/bin/blaze");
+    buildSystemProviderExtensionPoint.registerExtension(buildSystemProvider);
 
     configuration =
         new BlazeCommandRunConfigurationType().getFactory().createTemplateConfiguration(project);
@@ -93,7 +100,7 @@
                     configuration,
                     ProjectViewSet.builder().build(),
                     ImmutableList.of(),
-                    false /* debug */)
+                    /* debug */ false)
                 .toList())
         .isEqualTo(
             ImmutableList.of(
@@ -102,7 +109,6 @@
                 BlazeFlags.getToolTagFlag(),
                 "--flag1",
                 "--flag2",
-                "--test_output=streamed",
                 "--",
                 "//label:rule"));
   }
@@ -119,7 +125,7 @@
                     configuration,
                     ProjectViewSet.builder().build(),
                     ImmutableList.of(),
-                    true /* debug */)
+                    /* debug */ true)
                 .toList())
         .isEqualTo(
             ImmutableList.of(
@@ -143,7 +149,7 @@
                     configuration,
                     ProjectViewSet.builder().build(),
                     ImmutableList.of(),
-                    true /* debug */)
+                    /* debug */ true)
                 .toList())
         .isEqualTo(
             ImmutableList.of(
diff --git a/java/tests/unittests/com/google/idea/blaze/java/sync/importer/BlazeJavaWorkspaceImporterTest.java b/java/tests/unittests/com/google/idea/blaze/java/sync/importer/BlazeJavaWorkspaceImporterTest.java
index ad731d7..7ff7620 100644
--- a/java/tests/unittests/com/google/idea/blaze/java/sync/importer/BlazeJavaWorkspaceImporterTest.java
+++ b/java/tests/unittests/com/google/idea/blaze/java/sync/importer/BlazeJavaWorkspaceImporterTest.java
@@ -16,9 +16,7 @@
 package com.google.idea.blaze.java.sync.importer;
 
 import static com.google.common.truth.Truth.assertThat;
-import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertNotNull;
-import static org.junit.Assert.assertTrue;
 
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableSet;
@@ -83,8 +81,8 @@
 import java.util.Map;
 import java.util.function.Predicate;
 import java.util.stream.Collectors;
+import javax.annotation.Nullable;
 import org.jetbrains.annotations.NotNull;
-import org.jetbrains.annotations.Nullable;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.junit.runners.JUnit4;
@@ -94,7 +92,7 @@
 public class BlazeJavaWorkspaceImporterTest extends BlazeTestCase {
 
   private static final String FAKE_WORKSPACE_ROOT = "/root";
-  private WorkspaceRoot workspaceRoot = new WorkspaceRoot(new File(FAKE_WORKSPACE_ROOT));
+  private final WorkspaceRoot workspaceRoot = new WorkspaceRoot(new File(FAKE_WORKSPACE_ROOT));
 
   private static final String FAKE_GEN_ROOT_EXECUTION_PATH_FRAGMENT =
       "blaze-out/gcc-4.X.Y-crosstool-v17-hybrid-grtev3-k8-fastbuild/bin";
@@ -124,7 +122,7 @@
   }
 
   private BlazeContext context;
-  private ErrorCollector errorCollector = new ErrorCollector();
+  private final ErrorCollector errorCollector = new ErrorCollector();
   private final JdepsMock jdepsMap = new JdepsMock();
   private JavaWorkingSet workingSet = null;
   private final WorkspaceLanguageSettings workspaceLanguageSettings =
@@ -139,8 +137,7 @@
 
     BlazeExecutor blazeExecutor = new MockBlazeExecutor();
     applicationServices.register(BlazeExecutor.class, blazeExecutor);
-    projectServices.register(
-        BlazeImportSettingsManager.class, new BlazeImportSettingsManager(project));
+    projectServices.register(BlazeImportSettingsManager.class, new BlazeImportSettingsManager());
     BlazeImportSettingsManager.getInstance(getProject()).setImportSettings(DUMMY_IMPORT_SETTINGS);
 
     // will silently fall back to FilePathJavaPackageReader
@@ -195,7 +192,7 @@
     BlazeJavaImportResult result =
         importWorkspace(workspaceRoot, TargetMapBuilder.builder(), ProjectView.builder().build());
     errorCollector.assertNoIssues();
-    assertTrue(result.contentEntries.isEmpty());
+    assertThat(result.contentEntries).isEmpty();
   }
 
   @Test
@@ -233,10 +230,10 @@
     BlazeJavaImportResult result = importWorkspace(workspaceRoot, targetMapBuilder, projectView);
     errorCollector.assertNoIssues();
 
-    assertEquals(1, result.buildOutputJars.size());
+    assertThat(result.buildOutputJars).hasSize(1);
     ArtifactLocation compilerOutputLib = result.buildOutputJars.iterator().next();
     assertNotNull(compilerOutputLib);
-    assertTrue(compilerOutputLib.relativePath.endsWith("example_debug.jar"));
+    assertThat(compilerOutputLib.relativePath).endsWith("example_debug.jar");
 
     assertThat(result.contentEntries)
         .containsExactly(
@@ -668,7 +665,7 @@
 
     BlazeJavaImportResult result = importWorkspace(workspaceRoot, response, projectView);
     errorCollector.assertNoIssues();
-    assertEquals(1, result.libraries.size());
+    assertThat(result.libraries).hasSize(1);
   }
 
   @Test
@@ -713,7 +710,7 @@
     BlazeJavaImportResult result = importWorkspace(workspaceRoot, response, projectView);
     errorCollector.assertNoIssues();
 
-    assertEquals(1, result.libraries.size());
+    assertThat(result.libraries).hasSize(1);
   }
 
   @Test
@@ -761,7 +758,7 @@
 
     BlazeJavaImportResult result = importWorkspace(workspaceRoot, response, projectView);
     errorCollector.assertNoIssues();
-    assertEquals(1, result.libraries.size()); // The libraries were merged
+    assertThat(result.libraries).hasSize(1); // The libraries were merged
   }
 
   @Test
@@ -1394,7 +1391,7 @@
     return null;
   }
 
-  private ArtifactLocation source(String relativePath) {
+  private static ArtifactLocation source(String relativePath) {
     return ArtifactLocation.builder()
         .setRelativePath(relativePath)
         .setIsSource(true)
diff --git a/java/tests/unittests/com/google/idea/blaze/java/sync/source/SourceDirectoryCalculatorTest.java b/java/tests/unittests/com/google/idea/blaze/java/sync/source/SourceDirectoryCalculatorTest.java
index 25fac98..8545f55 100644
--- a/java/tests/unittests/com/google/idea/blaze/java/sync/source/SourceDirectoryCalculatorTest.java
+++ b/java/tests/unittests/com/google/idea/blaze/java/sync/source/SourceDirectoryCalculatorTest.java
@@ -71,12 +71,12 @@
   private MockInputStreamProvider mockInputStreamProvider;
   private SourceDirectoryCalculator sourceDirectoryCalculator;
 
-  private BlazeContext context = new BlazeContext();
-  private ErrorCollector issues = new ErrorCollector();
+  private final BlazeContext context = new BlazeContext();
+  private final ErrorCollector issues = new ErrorCollector();
   private MockExperimentService experimentService;
 
-  private WorkspaceRoot workspaceRoot = new WorkspaceRoot(new File("/root"));
-  private ArtifactLocationDecoder decoder =
+  private final WorkspaceRoot workspaceRoot = new WorkspaceRoot(new File("/root"));
+  private final ArtifactLocationDecoder decoder =
       (ArtifactLocationDecoder)
           artifactLocation -> new File("/root", artifactLocation.getRelativePath());
 
@@ -894,14 +894,12 @@
                 .build()),
         ImmutableList.of("com.google"));
     ImmutableMap<TargetKey, ArtifactLocation> manifests =
-        ImmutableMap.<TargetKey, ArtifactLocation>builder()
-            .put(
-                TargetKey.forPlainTarget(LABEL),
-                ArtifactLocation.builder()
-                    .setRelativePath("java/com/test.manifest")
-                    .setIsSource(true)
-                    .build())
-            .build();
+        ImmutableMap.of(
+            TargetKey.forPlainTarget(LABEL),
+            ArtifactLocation.builder()
+                .setRelativePath("java/com/test.manifest")
+                .setIsSource(true)
+                .build());
     Map<TargetKey, Map<ArtifactLocation, String>> manifestMap =
         readPackageManifestFiles(manifests, getDecoder("/root"));
 
@@ -921,14 +919,12 @@
         ImmutableList.of("java/com/google/Bla.java"),
         ImmutableList.of("com.google"));
     ImmutableMap<TargetKey, ArtifactLocation> manifests =
-        ImmutableMap.<TargetKey, ArtifactLocation>builder()
-            .put(
-                TargetKey.forPlainTarget(LABEL),
-                ArtifactLocation.builder()
-                    .setRelativePath("java/com/test.manifest")
-                    .setIsSource(true)
-                    .build())
-            .build();
+        ImmutableMap.of(
+            TargetKey.forPlainTarget(LABEL),
+            ArtifactLocation.builder()
+                .setRelativePath("java/com/test.manifest")
+                .setIsSource(true)
+                .build());
     Map<TargetKey, Map<ArtifactLocation, String>> manifestMap =
         readPackageManifestFiles(manifests, getDecoder("/root"));
 
@@ -1006,14 +1002,12 @@
         "package com.google.different;\n public class Bla {}");
 
     ImmutableMap<TargetKey, ArtifactLocation> manifests =
-        ImmutableMap.<TargetKey, ArtifactLocation>builder()
-            .put(
-                TargetKey.forPlainTarget(LABEL),
-                ArtifactLocation.builder()
-                    .setRelativePath("java/com/test.manifest")
-                    .setIsSource(true)
-                    .build())
-            .build();
+        ImmutableMap.of(
+            TargetKey.forPlainTarget(LABEL),
+            ArtifactLocation.builder()
+                .setRelativePath("java/com/test.manifest")
+                .setIsSource(true)
+                .build());
 
     List<SourceArtifact> sourceArtifacts =
         ImmutableList.of(
@@ -1099,17 +1093,15 @@
     BlazeRoots roots =
         new BlazeRoots(
             root,
-            ImmutableList.of(root),
             new ExecutionRootPath("out/crosstool/bin"),
             new ExecutionRootPath("out/crosstool/gen"),
             null);
-    return new ArtifactLocationDecoderImpl(
-        roots, new WorkspacePathResolverImpl(workspaceRoot, roots));
+    return new ArtifactLocationDecoderImpl(roots, new WorkspacePathResolverImpl(workspaceRoot));
   }
 
   private static class MockInputStreamProvider implements InputStreamProvider {
 
-    private final Map<String, InputStream> javaFiles = new HashMap<String, InputStream>();
+    private final Map<String, InputStream> javaFiles = new HashMap<>();
 
     public MockInputStreamProvider addFile(String filePath, String javaSrc) {
       try {
diff --git a/plugin_dev/src/com/google/idea/blaze/plugin/run/BlazeIntellijPluginConfiguration.java b/plugin_dev/src/com/google/idea/blaze/plugin/run/BlazeIntellijPluginConfiguration.java
index 528cac4..bee62dd 100644
--- a/plugin_dev/src/com/google/idea/blaze/plugin/run/BlazeIntellijPluginConfiguration.java
+++ b/plugin_dev/src/com/google/idea/blaze/plugin/run/BlazeIntellijPluginConfiguration.java
@@ -343,7 +343,8 @@
 
   protected BlazeCommand buildBlazeCommand(Project project, ProjectViewSet projectViewSet) {
     BlazeCommand.Builder command =
-        BlazeCommand.builder(Blaze.getBuildSystem(getProject()), BlazeCommandName.BUILD)
+        BlazeCommand.builder(
+                Blaze.getBuildSystemProvider(project).getBinaryPath(), BlazeCommandName.BUILD)
             .addTargets(getTarget())
             .addBlazeFlags(BlazeFlags.buildFlags(project, projectViewSet))
             .addBlazeFlags(blazeFlags.getFlags())
diff --git a/plugin_dev/src/com/google/idea/blaze/plugin/run/BlazeIntellijPluginDeployer.java b/plugin_dev/src/com/google/idea/blaze/plugin/run/BlazeIntellijPluginDeployer.java
index c06caf2..a75ffd2 100644
--- a/plugin_dev/src/com/google/idea/blaze/plugin/run/BlazeIntellijPluginDeployer.java
+++ b/plugin_dev/src/com/google/idea/blaze/plugin/run/BlazeIntellijPluginDeployer.java
@@ -45,7 +45,7 @@
 import java.util.Collection;
 import java.util.List;
 import java.util.Map;
-import org.jetbrains.annotations.Nullable;
+import javax.annotation.Nullable;
 
 /** Handles finding files to deploy and copying these into the sandbox. */
 class BlazeIntellijPluginDeployer {
diff --git a/proto_deps/BUILD b/proto/BUILD
similarity index 100%
rename from proto_deps/BUILD
rename to proto/BUILD
diff --git a/proto_deps/proto_deps.jar b/proto/proto_deps.jar
similarity index 100%
rename from proto_deps/proto_deps.jar
rename to proto/proto_deps.jar
Binary files differ
diff --git a/sdkcompat/BUILD b/sdkcompat/BUILD
index ce19ce5..a3b2a13 100644
--- a/sdkcompat/BUILD
+++ b/sdkcompat/BUILD
@@ -7,15 +7,14 @@
 java_library(
     name = "sdkcompat",
     srcs = select_for_plugin_api({
-        "android-studio-145.1617.8": glob(["v145/**"]),
-        "android-studio-2.3.0.3": glob(["v162/**"]),
-        "android-studio-2.3.0.4": glob(["v162/**"]),
-        "intellij-2016.3.1": glob(["v163/**"]),
-        "intellij-162.2032.8": glob(["v162/**"]),
-        "clion-162.1967.7": glob(
-            ["v162/**"],
-            exclude = ["v162/com/google/idea/sdkcompat/debugger/**"],
-        ),
+        "android-studio-145.1617.8": ["//sdkcompat/v145"],
+        "android-studio-2.3.0.3": ["//sdkcompat/v162"],
+        "android-studio-2.3.0.4": ["//sdkcompat/v162"],
+        "android-studio-2.3.0.6": ["//sdkcompat/v162"],
+        "intellij-2016.3.1": ["//sdkcompat/v163"],
+        "intellij-162.2032.8": ["//sdkcompat/v162"],
+        "clion-162.1967.7": ["//sdkcompat/v162"],
+        "clion-2016.3.2": ["//sdkcompat/v163"],
     }),
     visibility = ["//visibility:public"],
     deps = [
diff --git a/sdkcompat/v145/BUILD b/sdkcompat/v145/BUILD
new file mode 100644
index 0000000..a12549d
--- /dev/null
+++ b/sdkcompat/v145/BUILD
@@ -0,0 +1,23 @@
+# Description: Indirections for SDK changes to the underlying platform library.
+
+licenses(["notice"])  # Apache 2.0
+
+load("//intellij_platform_sdk:build_defs.bzl", "select_for_ide")
+
+filegroup(
+    name = "v145",
+    srcs = glob([
+        "com/google/idea/sdkcompat/codestyle/**",
+        "com/google/idea/sdkcompat/smrunner/**",
+        "com/google/idea/sdkcompat/transactions/**",
+        "com/google/idea/sdkcompat/vcs/**",
+    ]) + select_for_ide(
+        android_studio = glob([
+            "com/google/idea/sdkcompat/debugger/**",
+            "com/google/idea/sdkcompat/cidr/**",
+        ]),
+        clion = glob(["com/google/idea/sdkcompat/cidr/**"]),
+        intellij = glob(["com/google/idea/sdkcompat/debugger/**"]),
+    ),
+    visibility = ["//sdkcompat:__pkg__"],
+)
diff --git a/sdkcompat/v145/com/google/idea/sdkcompat/cidr/CidrConsoleBuilderAdapter.java b/sdkcompat/v145/com/google/idea/sdkcompat/cidr/CidrConsoleBuilderAdapter.java
new file mode 100644
index 0000000..66a868e
--- /dev/null
+++ b/sdkcompat/v145/com/google/idea/sdkcompat/cidr/CidrConsoleBuilderAdapter.java
@@ -0,0 +1,12 @@
+package com.google.idea.sdkcompat.cidr;
+
+import com.intellij.openapi.project.Project;
+import com.jetbrains.cidr.execution.CidrConsoleBuilder;
+
+/** Adapter to extend two bridge different SDK versions. */
+public class CidrConsoleBuilderAdapter extends CidrConsoleBuilder {
+
+  public CidrConsoleBuilderAdapter(Project project) {
+    super(project);
+  }
+}
diff --git a/sdkcompat/v145/com/google/idea/sdkcompat/cidr/OCWorkspaceAdapter.java b/sdkcompat/v145/com/google/idea/sdkcompat/cidr/OCWorkspaceAdapter.java
new file mode 100644
index 0000000..ccf31fe
--- /dev/null
+++ b/sdkcompat/v145/com/google/idea/sdkcompat/cidr/OCWorkspaceAdapter.java
@@ -0,0 +1,28 @@
+package com.google.idea.sdkcompat.cidr;
+
+import com.intellij.openapi.project.Project;
+import com.jetbrains.cidr.lang.workspace.OCResolveConfiguration;
+import com.jetbrains.cidr.lang.workspace.OCWorkspace;
+import com.jetbrains.cidr.lang.workspace.OCWorkspaceModificationTrackers;
+import javax.annotation.Nullable;
+
+/** Adapter to extend two bridge different SDK versions. */
+public abstract class OCWorkspaceAdapter implements OCWorkspace {
+
+  private final Project project;
+
+  protected OCWorkspaceAdapter(Project project) {
+    this.project = project;
+  }
+
+  @Nullable
+  @Override
+  public OCResolveConfiguration getSelectedResolveConfiguration() {
+    return null;
+  }
+
+  @Override
+  public OCWorkspaceModificationTrackers getModificationTrackers() {
+    return OCWorkspaceModificationTrackersCompatUtils.getTrackers(project);
+  }
+}
diff --git a/sdkcompat/v145/com/google/idea/sdkcompat/cidr/OCWorkspaceModificationTrackersCompatUtils.java b/sdkcompat/v145/com/google/idea/sdkcompat/cidr/OCWorkspaceModificationTrackersCompatUtils.java
new file mode 100644
index 0000000..4b17895
--- /dev/null
+++ b/sdkcompat/v145/com/google/idea/sdkcompat/cidr/OCWorkspaceModificationTrackersCompatUtils.java
@@ -0,0 +1,25 @@
+package com.google.idea.sdkcompat.cidr;
+
+import com.intellij.openapi.project.Project;
+import com.jetbrains.cidr.lang.workspace.OCWorkspaceModificationTrackers;
+import java.util.HashMap;
+import java.util.Map;
+
+/** Handles changes to modification trackers between our supported versions. */
+public class OCWorkspaceModificationTrackersCompatUtils {
+
+  private static final Map<Project, OCWorkspaceModificationTrackers> trackers = new HashMap<>();
+
+  public static OCWorkspaceModificationTrackers getTrackers(Project project) {
+    return trackers.computeIfAbsent(project, OCWorkspaceModificationTrackers::new);
+  }
+
+  /** Must be called inside a write action, on the EDT. */
+  public static void incrementModificationCounts(Project project) {
+    OCWorkspaceModificationTrackers modTrackers = getTrackers(project);
+    modTrackers.getProjectFilesListTracker().incModificationCount();
+    modTrackers.getSourceFilesListTracker().incModificationCount();
+    modTrackers.getBuildConfigurationChangesTracker().incModificationCount();
+    modTrackers.getBuildSettingsChangesTracker().incModificationCount();
+  }
+}
diff --git a/sdkcompat/v162/BUILD b/sdkcompat/v162/BUILD
new file mode 100644
index 0000000..f8ddfc1
--- /dev/null
+++ b/sdkcompat/v162/BUILD
@@ -0,0 +1,26 @@
+# Description: Indirections for SDK changes to the underlying platform library.
+
+licenses(["notice"])  # Apache 2.0
+
+load("//intellij_platform_sdk:build_defs.bzl", "select_for_ide")
+
+filegroup(
+    name = "v162",
+    srcs = glob([
+        "com/google/idea/sdkcompat/codestyle/**",
+        "com/google/idea/sdkcompat/smrunner/**",
+        "com/google/idea/sdkcompat/transactions/**",
+        "com/google/idea/sdkcompat/vcs/**",
+    ]) + select_for_ide(
+        android_studio = glob([
+            "com/google/idea/sdkcompat/debugger/**",
+            "com/google/idea/sdkcompat/cidr/**",
+        ]),
+        clion = glob([
+            "com/google/idea/sdkcompat/cidr/**",
+            "com/google/idea/sdkcompat/clion/**",
+        ]),
+        intellij = glob(["com/google/idea/sdkcompat/debugger/**"]),
+    ),
+    visibility = ["//sdkcompat:__pkg__"],
+)
diff --git a/sdkcompat/v162/com/google/idea/sdkcompat/cidr/CidrConsoleBuilderAdapter.java b/sdkcompat/v162/com/google/idea/sdkcompat/cidr/CidrConsoleBuilderAdapter.java
new file mode 100644
index 0000000..66a868e
--- /dev/null
+++ b/sdkcompat/v162/com/google/idea/sdkcompat/cidr/CidrConsoleBuilderAdapter.java
@@ -0,0 +1,12 @@
+package com.google.idea.sdkcompat.cidr;
+
+import com.intellij.openapi.project.Project;
+import com.jetbrains.cidr.execution.CidrConsoleBuilder;
+
+/** Adapter to extend two bridge different SDK versions. */
+public class CidrConsoleBuilderAdapter extends CidrConsoleBuilder {
+
+  public CidrConsoleBuilderAdapter(Project project) {
+    super(project);
+  }
+}
diff --git a/sdkcompat/v162/com/google/idea/sdkcompat/cidr/OCWorkspaceAdapter.java b/sdkcompat/v162/com/google/idea/sdkcompat/cidr/OCWorkspaceAdapter.java
new file mode 100644
index 0000000..ccf31fe
--- /dev/null
+++ b/sdkcompat/v162/com/google/idea/sdkcompat/cidr/OCWorkspaceAdapter.java
@@ -0,0 +1,28 @@
+package com.google.idea.sdkcompat.cidr;
+
+import com.intellij.openapi.project.Project;
+import com.jetbrains.cidr.lang.workspace.OCResolveConfiguration;
+import com.jetbrains.cidr.lang.workspace.OCWorkspace;
+import com.jetbrains.cidr.lang.workspace.OCWorkspaceModificationTrackers;
+import javax.annotation.Nullable;
+
+/** Adapter to extend two bridge different SDK versions. */
+public abstract class OCWorkspaceAdapter implements OCWorkspace {
+
+  private final Project project;
+
+  protected OCWorkspaceAdapter(Project project) {
+    this.project = project;
+  }
+
+  @Nullable
+  @Override
+  public OCResolveConfiguration getSelectedResolveConfiguration() {
+    return null;
+  }
+
+  @Override
+  public OCWorkspaceModificationTrackers getModificationTrackers() {
+    return OCWorkspaceModificationTrackersCompatUtils.getTrackers(project);
+  }
+}
diff --git a/sdkcompat/v162/com/google/idea/sdkcompat/cidr/OCWorkspaceModificationTrackersCompatUtils.java b/sdkcompat/v162/com/google/idea/sdkcompat/cidr/OCWorkspaceModificationTrackersCompatUtils.java
new file mode 100644
index 0000000..4b17895
--- /dev/null
+++ b/sdkcompat/v162/com/google/idea/sdkcompat/cidr/OCWorkspaceModificationTrackersCompatUtils.java
@@ -0,0 +1,25 @@
+package com.google.idea.sdkcompat.cidr;
+
+import com.intellij.openapi.project.Project;
+import com.jetbrains.cidr.lang.workspace.OCWorkspaceModificationTrackers;
+import java.util.HashMap;
+import java.util.Map;
+
+/** Handles changes to modification trackers between our supported versions. */
+public class OCWorkspaceModificationTrackersCompatUtils {
+
+  private static final Map<Project, OCWorkspaceModificationTrackers> trackers = new HashMap<>();
+
+  public static OCWorkspaceModificationTrackers getTrackers(Project project) {
+    return trackers.computeIfAbsent(project, OCWorkspaceModificationTrackers::new);
+  }
+
+  /** Must be called inside a write action, on the EDT. */
+  public static void incrementModificationCounts(Project project) {
+    OCWorkspaceModificationTrackers modTrackers = getTrackers(project);
+    modTrackers.getProjectFilesListTracker().incModificationCount();
+    modTrackers.getSourceFilesListTracker().incModificationCount();
+    modTrackers.getBuildConfigurationChangesTracker().incModificationCount();
+    modTrackers.getBuildSettingsChangesTracker().incModificationCount();
+  }
+}
diff --git a/sdkcompat/v162/com/google/idea/sdkcompat/clion/CMakeActionList.java b/sdkcompat/v162/com/google/idea/sdkcompat/clion/CMakeActionList.java
new file mode 100644
index 0000000..9f58cf3
--- /dev/null
+++ b/sdkcompat/v162/com/google/idea/sdkcompat/clion/CMakeActionList.java
@@ -0,0 +1,22 @@
+package com.google.idea.sdkcompat.clion;
+
+import com.google.common.collect.ImmutableSet;
+import com.jetbrains.cidr.cpp.cmake.actions.ChangeCMakeProjectContentRootAction;
+import com.jetbrains.cidr.cpp.cmake.actions.DropCMakeCacheAction;
+import com.jetbrains.cidr.cpp.cmake.actions.OpenCMakeSettingsAction;
+import com.jetbrains.cidr.cpp.cmake.actions.ReloadCMakeProjectAction;
+import com.jetbrains.cidr.cpp.cmake.actions.ToggleCMakeAutoReloadAction;
+
+/** Handles CMake actions which have changed between our supported versions. */
+public class CMakeActionList {
+
+  public static ImmutableSet<String> CMAKE_ACTION_IDS =
+      ImmutableSet.of(
+          ChangeCMakeProjectContentRootAction.ID,
+          DropCMakeCacheAction.ID,
+          OpenCMakeSettingsAction.ID,
+          ReloadCMakeProjectAction.ID,
+          ToggleCMakeAutoReloadAction.ID,
+          // 'CMake' > 'Show Generated CMake Files' action
+          "CMake.ShowGeneratedDir");
+}
diff --git a/sdkcompat/v163/BUILD b/sdkcompat/v163/BUILD
new file mode 100644
index 0000000..ca51bb8
--- /dev/null
+++ b/sdkcompat/v163/BUILD
@@ -0,0 +1,26 @@
+# Description: Indirections for SDK changes to the underlying platform library.
+
+licenses(["notice"])  # Apache 2.0
+
+load("//intellij_platform_sdk:build_defs.bzl", "select_for_ide")
+
+filegroup(
+    name = "v163",
+    srcs = glob([
+        "com/google/idea/sdkcompat/codestyle/**",
+        "com/google/idea/sdkcompat/smrunner/**",
+        "com/google/idea/sdkcompat/transactions/**",
+        "com/google/idea/sdkcompat/vcs/**",
+    ]) + select_for_ide(
+        android_studio = glob([
+            "com/google/idea/sdkcompat/debugger/**",
+            "com/google/idea/sdkcompat/cidr/**",
+        ]),
+        clion = glob([
+            "com/google/idea/sdkcompat/cidr/**",
+            "com/google/idea/sdkcompat/clion/**",
+        ]),
+        intellij = glob(["com/google/idea/sdkcompat/debugger/**"]),
+    ),
+    visibility = ["//sdkcompat:__pkg__"],
+)
diff --git a/sdkcompat/v163/com/google/idea/sdkcompat/cidr/CidrConsoleBuilderAdapter.java b/sdkcompat/v163/com/google/idea/sdkcompat/cidr/CidrConsoleBuilderAdapter.java
new file mode 100644
index 0000000..0ed4265
--- /dev/null
+++ b/sdkcompat/v163/com/google/idea/sdkcompat/cidr/CidrConsoleBuilderAdapter.java
@@ -0,0 +1,12 @@
+package com.google.idea.sdkcompat.cidr;
+
+import com.intellij.openapi.project.Project;
+import com.jetbrains.cidr.execution.CidrConsoleBuilder;
+
+/** Adapter to extend two bridge different SDK versions. */
+public class CidrConsoleBuilderAdapter extends CidrConsoleBuilder {
+
+  public CidrConsoleBuilderAdapter(Project project) {
+    super(project, null, null);
+  }
+}
diff --git a/sdkcompat/v163/com/google/idea/sdkcompat/cidr/OCWorkspaceAdapter.java b/sdkcompat/v163/com/google/idea/sdkcompat/cidr/OCWorkspaceAdapter.java
new file mode 100644
index 0000000..1c4d586
--- /dev/null
+++ b/sdkcompat/v163/com/google/idea/sdkcompat/cidr/OCWorkspaceAdapter.java
@@ -0,0 +1,9 @@
+package com.google.idea.sdkcompat.cidr;
+
+import com.intellij.openapi.project.Project;
+import com.jetbrains.cidr.lang.workspace.OCWorkspace;
+
+/** Adapter to extend two bridge different SDK versions. */
+public abstract class OCWorkspaceAdapter implements OCWorkspace {
+  protected OCWorkspaceAdapter(Project project) {}
+}
diff --git a/sdkcompat/v163/com/google/idea/sdkcompat/cidr/OCWorkspaceModificationTrackersCompatUtils.java b/sdkcompat/v163/com/google/idea/sdkcompat/cidr/OCWorkspaceModificationTrackersCompatUtils.java
new file mode 100644
index 0000000..1f38a39
--- /dev/null
+++ b/sdkcompat/v163/com/google/idea/sdkcompat/cidr/OCWorkspaceModificationTrackersCompatUtils.java
@@ -0,0 +1,21 @@
+package com.google.idea.sdkcompat.cidr;
+
+import com.intellij.openapi.project.Project;
+import com.jetbrains.cidr.lang.workspace.OCWorkspaceModificationTrackers;
+
+/** Handles changes to modification trackers between our supported versions. */
+public class OCWorkspaceModificationTrackersCompatUtils {
+
+  public static OCWorkspaceModificationTrackers getTrackers(Project project) {
+    return OCWorkspaceModificationTrackers.getInstance(project);
+  }
+
+  /** Must be called inside a write action, on the EDT. */
+  public static void incrementModificationCounts(Project project) {
+    OCWorkspaceModificationTrackers modTrackers = getTrackers(project);
+    modTrackers.getProjectFilesListTracker().incModificationCount();
+    modTrackers.getSourceFilesListTracker().incModificationCount();
+    modTrackers.getSelectedResolveConfigurationTracker().incModificationCount();
+    modTrackers.getBuildSettingsChangesTracker().incModificationCount();
+  }
+}
diff --git a/sdkcompat/v163/com/google/idea/sdkcompat/clion/CMakeActionList.java b/sdkcompat/v163/com/google/idea/sdkcompat/clion/CMakeActionList.java
new file mode 100644
index 0000000..007bd23
--- /dev/null
+++ b/sdkcompat/v163/com/google/idea/sdkcompat/clion/CMakeActionList.java
@@ -0,0 +1,22 @@
+package com.google.idea.sdkcompat.clion;
+
+import com.google.common.collect.ImmutableSet;
+import com.jetbrains.cidr.cpp.cmake.actions.ChangeCMakeProjectContentRootAction;
+import com.jetbrains.cidr.cpp.cmake.actions.ClearCMakeCacheAndReloadAction;
+import com.jetbrains.cidr.cpp.cmake.actions.OpenCMakeSettingsAction;
+import com.jetbrains.cidr.cpp.cmake.actions.ReloadCMakeProjectAction;
+import com.jetbrains.cidr.cpp.cmake.actions.ToggleCMakeAutoReloadAction;
+
+/** Handles CMake actions which have changed between our supported versions. */
+public class CMakeActionList {
+
+  public static ImmutableSet<String> CMAKE_ACTION_IDS =
+      ImmutableSet.of(
+          ChangeCMakeProjectContentRootAction.ID,
+          ClearCMakeCacheAndReloadAction.ID,
+          OpenCMakeSettingsAction.ID,
+          ReloadCMakeProjectAction.ID,
+          ToggleCMakeAutoReloadAction.ID,
+          // 'CMake' > 'Show Generated CMake Files' action
+          "CMake.ShowGeneratedDir");
+}
diff --git a/version.bzl b/version.bzl
index 85514a2..f56966e 100644
--- a/version.bzl
+++ b/version.bzl
@@ -1,3 +1,3 @@
 """Version of the blaze plugin."""
 
-VERSION = "2017.01.30.4"
+VERSION = "2017.02.13.1"