Import of bazel plugin using copybara

PiperOrigin-RevId: 145021219
diff --git a/BUILD b/BUILD
index f87b1dc..99282bc 100644
--- a/BUILD
+++ b/BUILD
@@ -10,6 +10,7 @@
     tests = [
         "//base:integration_tests",
         "//base:unit_tests",
+        "//golang:unit_tests",
         "//ijwb:integration_tests",
         "//ijwb:unit_tests",
         "//java:integration_tests",
diff --git a/WORKSPACE b/WORKSPACE
index edb340b..bad62f5 100644
--- a/WORKSPACE
+++ b/WORKSPACE
@@ -2,6 +2,14 @@
 
 # Long-lived download links available at: https://www.jetbrains.com/intellij-repository/releases
 
+# The plugin api for IntelliJ 2016.3.1. This is required to build IJwB,
+# and run integration tests.
+new_http_archive(
+    name = "intellij_ce_2016_3_1",
+    build_file = "intellij_platform_sdk/BUILD.idea",
+    url = "https://www.jetbrains.com/intellij-repository/releases/com/jetbrains/intellij/idea/ideaIC/2016.3.1/ideaIC-2016.3.1.zip",
+)
+
 # The plugin api for IntelliJ 2016.2.4. This is required to build IJwB,
 # and run integration tests.
 new_http_archive(
@@ -18,6 +26,30 @@
     url = "https://download.jetbrains.com/cpp/CLion-2016.2.2.tar.gz",
 )
 
+# The plugin api for CLion 2016.3.2. This is required to build CLwB,
+# and run integration tests.
+new_http_archive(
+    name = "clion_2016_3_2",
+    build_file = "intellij_platform_sdk/BUILD.clion",
+    url = "https://download.jetbrains.com/cpp/CLion-2016.3.2.tar.gz",
+)
+
+# The plugin api for Android Studio 2.3 Beta 1. This is required to build ASwB,
+# and run integration tests.
+new_http_archive(
+    name = "android_studio_2_3_0_3",
+    build_file = "intellij_platform_sdk/BUILD.android_studio",
+    url = "https://dl.google.com/dl/android/studio/ide-zips/2.3.0.3/android-studio-ide-162.3573574-linux.zip",
+)
+
+# The plugin api for Android Studio 2.3 Beta 2. This is required to build ASwB,
+# and run integration tests.
+new_http_archive(
+    name = "android_studio_2_3_0_4",
+    build_file = "intellij_platform_sdk/BUILD.android_studio",
+    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.2 stable. This is required to build ASwB,
 # and run integration tests.
 new_http_archive(
diff --git a/aswb/2.2/src/com/google/idea/blaze/android/compatibility/Compatibility.java b/aswb/2.2/src/com/google/idea/blaze/android/compatibility/Compatibility.java
new file mode 100644
index 0000000..d65d9f2
--- /dev/null
+++ b/aswb/2.2/src/com/google/idea/blaze/android/compatibility/Compatibility.java
@@ -0,0 +1,138 @@
+/*
+ * 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.compatibility;
+
+import com.android.sdklib.AndroidVersion;
+import com.android.tools.idea.run.ConsolePrinter;
+import com.android.tools.idea.run.editor.AndroidDebugger;
+import com.android.tools.idea.run.editor.AndroidDebuggerState;
+import com.android.tools.idea.run.tasks.DebugConnectorTask;
+import com.android.tools.idea.run.util.LaunchStatus;
+import com.intellij.execution.Executor;
+import com.intellij.execution.configurations.ConfigurationFactory;
+import com.intellij.execution.configurations.RunConfiguration;
+import com.intellij.execution.runners.ExecutionEnvironment;
+import com.intellij.openapi.project.Project;
+import com.intellij.openapi.projectRoots.Sdk;
+import java.io.File;
+import java.util.List;
+import java.util.Set;
+import org.jetbrains.android.facet.AndroidFacet;
+import org.jetbrains.android.sdk.AndroidSdkAdditionalData;
+import org.jetbrains.annotations.Nullable;
+import org.jetbrains.jps.android.model.impl.JpsAndroidModuleProperties;
+
+/** Compatibility facades for Android Studio 2.2. */
+public class Compatibility {
+  private Compatibility() {}
+  /**
+   * Facade for {@link org.jetbrains.android.sdk.AndroidSdkUtils} and {@link
+   * com.android.tools.idea.sdk.AndroidSdks#getInstance()}.
+   */
+  public static class AndroidSdkUtils {
+    private AndroidSdkUtils() {}
+
+    public static Sdk findSuitableAndroidSdk(String targetHash) {
+      return org.jetbrains.android.sdk.AndroidSdkUtils.findSuitableAndroidSdk(targetHash);
+    }
+
+    public static List<Sdk> getAllAndroidSdks() {
+      return org.jetbrains.android.sdk.AndroidSdkUtils.getAllAndroidSdks();
+    }
+
+    public static AndroidSdkAdditionalData getAndroidSdkAdditionalData(Sdk sdk) {
+      return org.jetbrains.android.sdk.AndroidSdkUtils.getAndroidSdkAdditionalData(sdk);
+    }
+  }
+
+  /**
+   * Facade for {@link com.android.tools.idea.sdk.IdeSdks} and {@link
+   * com.android.tools.idea.sdk.IdeSdks#getInstance()}.
+   */
+  public static class IdeSdks {
+    private IdeSdks() {}
+
+    public static File getAndroidSdkPath() {
+      return com.android.tools.idea.sdk.IdeSdks.getAndroidSdkPath();
+    }
+
+    public static List<Sdk> createAndroidSdkPerAndroidTarget(File androidSdkPath) {
+      return com.android.tools.idea.sdk.IdeSdks.createAndroidSdkPerAndroidTarget(androidSdkPath);
+    }
+  }
+
+  /**
+   * Facade for {@link com.android.tools.idea.run.testing.AndroidTestListener} and {@link
+   * com.android.tools.idea.testartifacts.instrumented.AndroidTestListener}
+   */
+  public static class AndroidTestListener
+      extends com.android.tools.idea.run.testing.AndroidTestListener {
+    public AndroidTestListener(LaunchStatus launchStatus, ConsolePrinter consolePrinter) {
+      super(launchStatus, consolePrinter);
+    }
+  }
+
+  /**
+   * Facade for {@link com.android.tools.idea.run.testing.AndroidTestConsoleProperties} and {@link
+   * com.android.tools.idea.testartifacts.instrumented.AndroidTestConsoleProperties}
+   */
+  public static class AndroidTestConsoleProperties
+      extends com.android.tools.idea.run.testing.AndroidTestConsoleProperties {
+    public AndroidTestConsoleProperties(RunConfiguration runConfiguration, Executor executor) {
+      super(runConfiguration, executor);
+    }
+  }
+
+  /**
+   * Facade for {@link com.android.tools.idea.run.testing.AndroidTestRunConfiguration} and {@link
+   * com.android.tools.idea.testartifacts.instrumented.AndroidTestRunConfiguration}
+   */
+  public static class AndroidTestRunConfiguration
+      extends com.android.tools.idea.run.testing.AndroidTestRunConfiguration {
+    public AndroidTestRunConfiguration(Project project, ConfigurationFactory configurationFactory) {
+      super(project, configurationFactory);
+    }
+  }
+
+  /** Facade for {@link com.android.tools.idea.run.tasks.ConnectDebuggerTask}. */
+  public abstract static class ConnectDebuggerTask
+      extends com.android.tools.idea.run.tasks.ConnectDebuggerTask {
+    protected ConnectDebuggerTask(
+        Set<String> applicationIds,
+        AndroidDebugger<?> debugger,
+        Project project,
+        boolean monitorRemoteProcess) {
+      super(applicationIds, debugger, project);
+    }
+  }
+
+  public static <S extends AndroidDebuggerState> DebugConnectorTask getConnectDebuggerTask(
+      AndroidDebugger<S> androidDebugger,
+      ExecutionEnvironment env,
+      @Nullable AndroidVersion version,
+      Set<String> applicationIds,
+      AndroidFacet facet,
+      S state,
+      String runConfigTypeId,
+      boolean monitorRemoteProcess) {
+    return androidDebugger.getConnectDebuggerTask(
+        env, version, applicationIds, facet, state, runConfigTypeId);
+  }
+
+  public static void setFacetStateIsLibraryProject(JpsAndroidModuleProperties facetState) {
+    facetState.LIBRARY_PROJECT = true;
+  }
+}
diff --git a/aswb/src/com/google/idea/blaze/android/sync/model/idea/NullClassJarProvider.java b/aswb/2.2/src/com/google/idea/blaze/android/sync/model/idea/BlazeClassJarProvider.java
similarity index 78%
rename from aswb/src/com/google/idea/blaze/android/sync/model/idea/NullClassJarProvider.java
rename to aswb/2.2/src/com/google/idea/blaze/android/sync/model/idea/BlazeClassJarProvider.java
index 037f4fa..20c2c22 100644
--- a/aswb/src/com/google/idea/blaze/android/sync/model/idea/NullClassJarProvider.java
+++ b/aswb/2.2/src/com/google/idea/blaze/android/sync/model/idea/BlazeClassJarProvider.java
@@ -18,22 +18,23 @@
 import com.android.tools.idea.model.ClassJarProvider;
 import com.google.common.collect.ImmutableList;
 import com.intellij.openapi.module.Module;
+import com.intellij.openapi.project.Project;
 import com.intellij.openapi.vfs.VirtualFile;
 import java.util.List;
-import org.jetbrains.annotations.NotNull;
 import org.jetbrains.annotations.Nullable;
 
 /** Returns no class jars. Used to disable the layout editor loading jars. */
-public class NullClassJarProvider extends ClassJarProvider {
+public class BlazeClassJarProvider extends ClassJarProvider {
+  public BlazeClassJarProvider(Project project) {}
+
   @Nullable
   @Override
-  public VirtualFile findModuleClassFile(@NotNull String className, @NotNull Module module) {
+  public VirtualFile findModuleClassFile(String className, Module module) {
     return null;
   }
 
-  @NotNull
   @Override
-  public List<VirtualFile> getModuleExternalLibraries(@NotNull Module module) {
+  public List<VirtualFile> getModuleExternalLibraries(Module module) {
     return ImmutableList.of();
   }
 }
diff --git a/aswb/tests/utils/integration/com/google/idea/blaze/android/AndroidTestCleanupHelper.java b/aswb/2.2/tests/utils/integration/com/google/idea/blaze/android/AndroidTestCleanupHelper.java
similarity index 100%
rename from aswb/tests/utils/integration/com/google/idea/blaze/android/AndroidTestCleanupHelper.java
rename to aswb/2.2/tests/utils/integration/com/google/idea/blaze/android/AndroidTestCleanupHelper.java
diff --git a/aswb/tests/utils/integration/com/google/idea/blaze/android/AndroidTestSetupRule.java b/aswb/2.2/tests/utils/integration/com/google/idea/blaze/android/AndroidTestSetupRule.java
similarity index 100%
rename from aswb/tests/utils/integration/com/google/idea/blaze/android/AndroidTestSetupRule.java
rename to aswb/2.2/tests/utils/integration/com/google/idea/blaze/android/AndroidTestSetupRule.java
diff --git a/aswb/tests/utils/integration/com/google/idea/blaze/android/BlazeAndroidIntegrationTestCase.java b/aswb/2.2/tests/utils/integration/com/google/idea/blaze/android/BlazeAndroidIntegrationTestCase.java
similarity index 100%
rename from aswb/tests/utils/integration/com/google/idea/blaze/android/BlazeAndroidIntegrationTestCase.java
rename to aswb/2.2/tests/utils/integration/com/google/idea/blaze/android/BlazeAndroidIntegrationTestCase.java
diff --git a/aswb/2.3/src/META-INF/aswb_beta.xml b/aswb/2.3/src/META-INF/aswb_beta.xml
new file mode 100644
index 0000000..8df2945
--- /dev/null
+++ b/aswb/2.3/src/META-INF/aswb_beta.xml
@@ -0,0 +1,35 @@
+<!--
+  ~ 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.
+  -->
+<idea-plugin>
+  <depends>com.google.gct.test.recorder</depends>
+
+  <extensions defaultExtensionNs="com.google.idea.blaze">
+    <BlazeUserSettingsContributor implementation="com.google.idea.blaze.android.settings.BlazeAndroidUserSettingsContributor$BlazeAndroidUserSettingsProvider"/>
+  </extensions>
+
+  <extensions defaultExtensionNs="com.android.project">
+    <buildSystemService implementation="com.google.idea.blaze.android.project.BlazeBuildSystemService"/>
+    <featureEnableService implementation="com.google.idea.blaze.android.project.BlazeFeatureEnableService"/>
+  </extensions>
+
+  <extensions defaultExtensionNs="com.android.rendering">
+    <renderErrorContributor implementation="com.google.idea.blaze.android.rendering.BlazeRenderErrorContributor$BlazeProvider"/>
+  </extensions>
+
+  <extensions defaultExtensionNs="com.google.gct.testrecorder.run">
+    <testRecorderRunConfigurationProxyProvider implementation="com.google.idea.blaze.android.run.testrecorder.TestRecorderBlazeCommandRunConfigurationProxyProvider" />
+  </extensions>
+</idea-plugin>
diff --git a/aswb/2.3/src/com/google/idea/blaze/android/compatibility/Compatibility.java b/aswb/2.3/src/com/google/idea/blaze/android/compatibility/Compatibility.java
new file mode 100644
index 0000000..e614959
--- /dev/null
+++ b/aswb/2.3/src/com/google/idea/blaze/android/compatibility/Compatibility.java
@@ -0,0 +1,141 @@
+/*
+ * 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.compatibility;
+
+import com.android.sdklib.AndroidVersion;
+import com.android.tools.idea.run.ConsolePrinter;
+import com.android.tools.idea.run.editor.AndroidDebugger;
+import com.android.tools.idea.run.editor.AndroidDebuggerState;
+import com.android.tools.idea.run.tasks.DebugConnectorTask;
+import com.android.tools.idea.run.util.LaunchStatus;
+import com.intellij.execution.Executor;
+import com.intellij.execution.configurations.ConfigurationFactory;
+import com.intellij.execution.configurations.RunConfiguration;
+import com.intellij.execution.runners.ExecutionEnvironment;
+import com.intellij.openapi.project.Project;
+import com.intellij.openapi.projectRoots.Sdk;
+import java.io.File;
+import java.util.List;
+import java.util.Set;
+import org.jetbrains.android.facet.AndroidFacet;
+import org.jetbrains.android.sdk.AndroidSdkAdditionalData;
+import org.jetbrains.annotations.Nullable;
+import org.jetbrains.jps.android.model.impl.JpsAndroidModuleProperties;
+
+/** Compatibility facades for Android Studio 2.3. */
+public class Compatibility {
+  private Compatibility() {}
+
+  /**
+   * Facade for {@link org.jetbrains.android.sdk.AndroidSdkUtils} and {@link
+   * com.android.tools.idea.sdk.AndroidSdks#getInstance()}.
+   */
+  public static class AndroidSdkUtils {
+    private AndroidSdkUtils() {}
+
+    public static Sdk findSuitableAndroidSdk(String targetHash) {
+      return com.android.tools.idea.sdk.AndroidSdks.getInstance()
+          .findSuitableAndroidSdk(targetHash);
+    }
+
+    public static List<Sdk> getAllAndroidSdks() {
+      return com.android.tools.idea.sdk.AndroidSdks.getInstance().getAllAndroidSdks();
+    }
+
+    public static AndroidSdkAdditionalData getAndroidSdkAdditionalData(Sdk sdk) {
+      return com.android.tools.idea.sdk.AndroidSdks.getInstance().getAndroidSdkAdditionalData(sdk);
+    }
+  }
+
+  /**
+   * Facade for {@link com.android.tools.idea.sdk.IdeSdks} and {@link
+   * com.android.tools.idea.sdk.IdeSdks#getInstance()}.
+   */
+  public static class IdeSdks {
+    private IdeSdks() {}
+
+    public static File getAndroidSdkPath() {
+      return com.android.tools.idea.sdk.IdeSdks.getInstance().getAndroidSdkPath();
+    }
+
+    public static List<Sdk> createAndroidSdkPerAndroidTarget(File androidSdkPath) {
+      return com.android.tools.idea.sdk.IdeSdks.getInstance()
+          .createAndroidSdkPerAndroidTarget(androidSdkPath);
+    }
+  }
+
+  /**
+   * Facade for {@link com.android.tools.idea.run.testing.AndroidTestListener} and {@link
+   * com.android.tools.idea.testartifacts.instrumented.AndroidTestListener}
+   */
+  public static class AndroidTestListener
+      extends com.android.tools.idea.testartifacts.instrumented.AndroidTestListener {
+    public AndroidTestListener(LaunchStatus launchStatus, ConsolePrinter consolePrinter) {
+      super(launchStatus, consolePrinter);
+    }
+  }
+
+  /**
+   * Facade for {@link com.android.tools.idea.run.testing.AndroidTestConsoleProperties} and {@link
+   * com.android.tools.idea.testartifacts.instrumented.AndroidTestConsoleProperties}
+   */
+  public static class AndroidTestConsoleProperties
+      extends com.android.tools.idea.testartifacts.instrumented.AndroidTestConsoleProperties {
+    public AndroidTestConsoleProperties(RunConfiguration runConfiguration, Executor executor) {
+      super(runConfiguration, executor);
+    }
+  }
+
+  /**
+   * Facade for {@link com.android.tools.idea.run.testing.AndroidTestRunConfiguration} and {@link
+   * com.android.tools.idea.testartifacts.instrumented.AndroidTestRunConfiguration}
+   */
+  public static class AndroidTestRunConfiguration
+      extends com.android.tools.idea.testartifacts.instrumented.AndroidTestRunConfiguration {
+    public AndroidTestRunConfiguration(Project project, ConfigurationFactory configurationFactory) {
+      super(project, configurationFactory);
+    }
+  }
+
+  /** Facade for {@link com.android.tools.idea.run.tasks.ConnectDebuggerTask}. */
+  public abstract static class ConnectDebuggerTask
+      extends com.android.tools.idea.run.tasks.ConnectDebuggerTask {
+    protected ConnectDebuggerTask(
+        Set<String> applicationIds,
+        AndroidDebugger<?> debugger,
+        Project project,
+        boolean monitorRemoteProcess) {
+      super(applicationIds, debugger, project, monitorRemoteProcess);
+    }
+  }
+
+  public static <S extends AndroidDebuggerState> DebugConnectorTask getConnectDebuggerTask(
+      AndroidDebugger<S> androidDebugger,
+      ExecutionEnvironment env,
+      @Nullable AndroidVersion version,
+      Set<String> applicationIds,
+      AndroidFacet facet,
+      S state,
+      String runConfigTypeId,
+      boolean monitorRemoteProcess) {
+    return androidDebugger.getConnectDebuggerTask(
+        env, version, applicationIds, facet, state, runConfigTypeId, monitorRemoteProcess);
+  }
+
+  public static void setFacetStateIsLibraryProject(JpsAndroidModuleProperties facetState) {
+    facetState.PROJECT_TYPE = com.android.builder.model.AndroidProject.PROJECT_TYPE_LIBRARY;
+  }
+}
diff --git a/aswb/2.3/src/com/google/idea/blaze/android/project/BlazeBuildSystemService.java b/aswb/2.3/src/com/google/idea/blaze/android/project/BlazeBuildSystemService.java
new file mode 100644
index 0000000..36923d7
--- /dev/null
+++ b/aswb/2.3/src/com/google/idea/blaze/android/project/BlazeBuildSystemService.java
@@ -0,0 +1,99 @@
+/*
+ * 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.project;
+
+import com.android.tools.idea.project.BuildSystemService;
+import com.google.idea.blaze.android.sync.model.AndroidResourceModuleRegistry;
+import com.google.idea.blaze.base.actions.BlazeBuildService;
+import com.google.idea.blaze.base.ideinfo.TargetIdeInfo;
+import com.google.idea.blaze.base.lang.buildfile.references.BuildReferenceManager;
+import com.google.idea.blaze.base.model.BlazeProjectData;
+import com.google.idea.blaze.base.settings.Blaze;
+import com.google.idea.blaze.base.sync.BlazeSyncManager;
+import com.google.idea.blaze.base.sync.data.BlazeProjectDataManager;
+import com.intellij.openapi.application.ApplicationManager;
+import com.intellij.openapi.fileEditor.FileEditorManager;
+import com.intellij.openapi.fileEditor.OpenFileDescriptor;
+import com.intellij.openapi.module.Module;
+import com.intellij.openapi.project.Project;
+import com.intellij.openapi.vfs.VfsUtil;
+import com.intellij.openapi.vfs.VirtualFile;
+import com.intellij.openapi.vfs.ex.temp.TempFileSystem;
+import com.intellij.psi.PsiElement;
+import java.io.File;
+
+/** Blaze implementation of {@link BuildSystemService} for build system specific operations. */
+public class BlazeBuildSystemService extends BuildSystemService {
+  @Override
+  public boolean isApplicable(Project project) {
+    return Blaze.isBlazeProject(project);
+  }
+
+  @Override
+  public void buildProject(Project project) {
+    BlazeBuildService.getInstance().buildProject(project);
+  }
+
+  @Override
+  public void syncProject(Project project) {
+    BlazeSyncManager.getInstance(project).incrementalProjectSync();
+  }
+
+  @Override
+  public void addDependency(Module module, String artifact) {
+    Project project = module.getProject();
+    BlazeProjectData blazeProjectData =
+        BlazeProjectDataManager.getInstance(project).getBlazeProjectData();
+    if (blazeProjectData == null) {
+      return;
+    }
+    AndroidResourceModuleRegistry registry = AndroidResourceModuleRegistry.getInstance(project);
+    TargetIdeInfo targetIdeInfo = blazeProjectData.targetMap.get(registry.getTargetKey(module));
+    if (targetIdeInfo == null || targetIdeInfo.buildFile == null) {
+      return;
+    }
+
+    // TODO: automagically edit deps instead of just opening the BUILD file?
+    // Need to translate Gradle coordinates into blaze targets.
+    // Will probably need to hardcode for each dependency.
+    FileEditorManager fileEditorManager = FileEditorManager.getInstance(project);
+    PsiElement buildTargetPsi =
+        BuildReferenceManager.getInstance(project).resolveLabel(targetIdeInfo.key.label);
+    if (buildTargetPsi != null) {
+      // If we can find a PSI for the target,
+      // then we can jump straight to the target in the build file.
+      fileEditorManager.openTextEditor(
+          new OpenFileDescriptor(
+              project,
+              buildTargetPsi.getContainingFile().getVirtualFile(),
+              buildTargetPsi.getTextOffset()),
+          true);
+    } else {
+      // If not, just the build file is good enough.
+      File buildIoFile = blazeProjectData.artifactLocationDecoder.decode(targetIdeInfo.buildFile);
+      VirtualFile buildVirtualFile = findFileByIoFile(buildIoFile);
+      if (buildVirtualFile != null) {
+        fileEditorManager.openFile(buildVirtualFile, true);
+      }
+    }
+  }
+
+  private static VirtualFile findFileByIoFile(File file) {
+    return ApplicationManager.getApplication().isUnitTestMode()
+        ? TempFileSystem.getInstance().findFileByIoFile(file)
+        : VfsUtil.findFileByIoFile(file, true);
+  }
+}
diff --git a/aswb/2.3/src/com/google/idea/blaze/android/project/BlazeFeatureEnableService.java b/aswb/2.3/src/com/google/idea/blaze/android/project/BlazeFeatureEnableService.java
new file mode 100644
index 0000000..e59f375
--- /dev/null
+++ b/aswb/2.3/src/com/google/idea/blaze/android/project/BlazeFeatureEnableService.java
@@ -0,0 +1,46 @@
+/*
+ * 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.project;
+
+import com.android.tools.idea.project.FeatureEnableService;
+import com.google.idea.blaze.android.settings.BlazeAndroidUserSettings;
+import com.google.idea.blaze.base.settings.Blaze;
+import com.google.idea.blaze.base.sync.data.BlazeProjectDataManager;
+import com.google.idea.common.experiments.BoolExperiment;
+import com.intellij.openapi.project.Project;
+
+/** Enable features supported by the blaze integration. */
+public class BlazeFeatureEnableService extends FeatureEnableService {
+  private static final BoolExperiment ENABLE_LAYOUT_EDITOR =
+      new BoolExperiment("enable.layout.editor", true);
+
+  @Override
+  protected boolean isApplicable(Project project) {
+    return Blaze.isBlazeProject(project);
+  }
+
+  @Override
+  public boolean isLayoutEditorEnabled(Project project) {
+    return isLayoutEditorExperimentEnabled()
+        && BlazeAndroidUserSettings.getInstance().getUseLayoutEditor()
+        // Can't render if we don't have the data ready.
+        && BlazeProjectDataManager.getInstance(project).getBlazeProjectData() != null;
+  }
+
+  public static boolean isLayoutEditorExperimentEnabled() {
+    return ENABLE_LAYOUT_EDITOR.getValue();
+  }
+}
diff --git a/aswb/2.3/src/com/google/idea/blaze/android/rendering/BlazeRenderErrorContributor.java b/aswb/2.3/src/com/google/idea/blaze/android/rendering/BlazeRenderErrorContributor.java
new file mode 100644
index 0000000..b4c8498
--- /dev/null
+++ b/aswb/2.3/src/com/google/idea/blaze/android/rendering/BlazeRenderErrorContributor.java
@@ -0,0 +1,338 @@
+/*
+ * 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.rendering;
+
+import static com.android.SdkConstants.ANDROID_MANIFEST_XML;
+
+import com.android.tools.idea.rendering.HtmlLinkManager;
+import com.android.tools.idea.rendering.RenderErrorContributor;
+import com.android.tools.idea.rendering.RenderLogger;
+import com.android.tools.idea.rendering.RenderResult;
+import com.android.tools.idea.rendering.errors.ui.RenderErrorModel;
+import com.android.utils.HtmlBuilder;
+import com.google.common.collect.ImmutableCollection;
+import com.google.common.collect.Maps;
+import com.google.common.collect.SortedSetMultimap;
+import com.google.common.collect.TreeMultimap;
+import com.google.idea.blaze.android.sync.model.AndroidResourceModule;
+import com.google.idea.blaze.android.sync.model.AndroidResourceModuleRegistry;
+import com.google.idea.blaze.base.ideinfo.ArtifactLocation;
+import com.google.idea.blaze.base.ideinfo.TargetIdeInfo;
+import com.google.idea.blaze.base.ideinfo.TargetKey;
+import com.google.idea.blaze.base.ideinfo.TargetMap;
+import com.google.idea.blaze.base.lang.buildfile.references.BuildReferenceManager;
+import com.google.idea.blaze.base.model.BlazeProjectData;
+import com.google.idea.blaze.base.settings.Blaze;
+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;
+import com.google.idea.blaze.base.targetmaps.TransitiveDependencyMap;
+import com.intellij.lang.annotation.HighlightSeverity;
+import com.intellij.openapi.actionSystem.DataContext;
+import com.intellij.openapi.application.ApplicationManager;
+import com.intellij.openapi.module.Module;
+import com.intellij.openapi.project.IndexNotReadyException;
+import com.intellij.openapi.project.Project;
+import com.intellij.openapi.util.Computable;
+import com.intellij.openapi.util.text.StringUtil;
+import com.intellij.openapi.vfs.VfsUtilCore;
+import com.intellij.psi.JavaPsiFacade;
+import com.intellij.psi.PsiClass;
+import com.intellij.psi.PsiElement;
+import com.intellij.psi.search.GlobalSearchScope;
+import java.io.File;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Map;
+import java.util.SortedMap;
+import java.util.function.Function;
+import java.util.stream.Collectors;
+import org.jetbrains.annotations.Nullable;
+
+/** Contribute blaze specific render errors. */
+public class BlazeRenderErrorContributor extends RenderErrorContributor {
+  private RenderLogger logger;
+  private Module module;
+  private Project project;
+
+  public BlazeRenderErrorContributor(RenderResult result, @Nullable DataContext dataContext) {
+    super(result, dataContext);
+    logger = result.getLogger();
+    module = result.getModule();
+    project = module.getProject();
+  }
+
+  @Override
+  public Collection<RenderErrorModel.Issue> reportIssues() {
+    BlazeProjectData blazeProjectData =
+        BlazeProjectDataManager.getInstance(project).getBlazeProjectData();
+
+    if (blazeProjectData == null || !logger.hasErrors()) {
+      return getIssues();
+    }
+
+    TargetMap targetMap = blazeProjectData.targetMap;
+    ArtifactLocationDecoder decoder = blazeProjectData.artifactLocationDecoder;
+    AndroidResourceModule resourceModule =
+        AndroidResourceModuleRegistry.getInstance(project).get(module);
+    if (resourceModule == null) {
+      return getIssues();
+    }
+
+    TargetIdeInfo target = targetMap.get(resourceModule.targetKey);
+    if (target == null) {
+      return getIssues();
+    }
+
+    reportGeneratedResources(resourceModule, targetMap, decoder);
+    reportNonStandardAndroidManifestName(target, decoder);
+    reportResourceTargetShouldDependOnClassTarget(target, targetMap, decoder);
+    return getIssues();
+  }
+
+  /**
+   * We can't find generated resources. If a layout uses them, the layout won't render correctly.
+   */
+  private void reportGeneratedResources(
+      AndroidResourceModule resourceModule, TargetMap targetMap, ArtifactLocationDecoder decoder) {
+    Map<String, Throwable> brokenClasses = logger.getBrokenClasses();
+    if (brokenClasses == null || brokenClasses.isEmpty()) {
+      return;
+    }
+
+    // Sorted entries for deterministic error message.
+    SortedMap<ArtifactLocation, TargetIdeInfo> generatedResources =
+        Maps.newTreeMap(getGeneratedResources(targetMap.get(resourceModule.targetKey)));
+
+    for (TargetKey dependency : resourceModule.transitiveResourceDependencies) {
+      generatedResources.putAll(getGeneratedResources(targetMap.get(dependency)));
+    }
+
+    if (generatedResources.isEmpty()) {
+      return;
+    }
+
+    HtmlBuilder builder = new HtmlBuilder();
+    builder.add("Generated resources will not be discovered by the IDE:");
+    builder.beginList();
+    for (Map.Entry<ArtifactLocation, TargetIdeInfo> entry : generatedResources.entrySet()) {
+      ArtifactLocation resource = entry.getKey();
+      TargetIdeInfo target = entry.getValue();
+      builder.listItem().add(resource.getRelativePath()).add(" from ");
+      addTargetLink(builder, target, decoder);
+    }
+    builder
+        .endList()
+        .add("Please avoid using generated resources, ")
+        .addLink("then ", "sync the project", " ", getLinkManager().createSyncProjectUrl())
+        .addLink("and ", "refresh the layout", ".", getLinkManager().createRefreshRenderUrl());
+    addIssue()
+        .setSeverity(HighlightSeverity.ERROR, HIGH_PRIORITY + 1) // Reported above broken classes
+        .setSummary("Generated resources")
+        .setHtmlContent(builder)
+        .build();
+  }
+
+  private static SortedMap<ArtifactLocation, TargetIdeInfo> getGeneratedResources(
+      TargetIdeInfo target) {
+    if (target == null || target.androidIdeInfo == null) {
+      return Collections.emptySortedMap();
+    }
+    SortedMap<ArtifactLocation, TargetIdeInfo> generatedResources = Maps.newTreeMap();
+    generatedResources.putAll(
+        target
+            .androidIdeInfo
+            .resources
+            .stream()
+            .filter(ArtifactLocation::isGenerated)
+            .collect(Collectors.toMap(Function.identity(), resource -> target)));
+    return generatedResources;
+  }
+
+  /**
+   * When the Android manifest isn't AndroidManifest.xml, resolving resource IDs would fail. This
+   * doesn't seem to be an issue if the manifest belongs to one of the target's dependencies.
+   */
+  private void reportNonStandardAndroidManifestName(
+      TargetIdeInfo target, ArtifactLocationDecoder decoder) {
+    if (target.androidIdeInfo == null || target.androidIdeInfo.manifest == null) {
+      return;
+    }
+
+    Map<String, Throwable> brokenClasses = logger.getBrokenClasses();
+    if (brokenClasses == null || brokenClasses.isEmpty()) {
+      return;
+    }
+
+    File manifest = decoder.decode(target.androidIdeInfo.manifest);
+    if (manifest.getName().equals(ANDROID_MANIFEST_XML)) {
+      return;
+    }
+
+    HtmlBuilder builder = new HtmlBuilder();
+    addTargetLink(builder, target, decoder)
+        .add(" uses a non-standard name for the Android manifest: ");
+    String linkToManifest = HtmlLinkManager.createFilePositionUrl(manifest, -1, 0);
+    if (linkToManifest != null) {
+      builder.addLink(manifest.getName(), linkToManifest);
+    } else {
+      builder.newline().add(manifest.getPath());
+    }
+    // TODO: add a link to automatically rename the file and refactor all references.
+    builder
+        .newline()
+        .add("Please rename it to ")
+        .add(ANDROID_MANIFEST_XML)
+        .addLink(", then ", "sync the project", "", getLinkManager().createSyncProjectUrl())
+        .addLink(" and ", "refresh the layout", ".", getLinkManager().createRefreshRenderUrl());
+    addIssue()
+        .setSeverity(HighlightSeverity.ERROR, HIGH_PRIORITY + 1) // Reported above broken classes.
+        .setSummary("Non-standard manifest name")
+        .setHtmlContent(builder)
+        .build();
+  }
+
+  /**
+   * Blaze doesn't resolve class dependencies from resources until building the final
+   * android_binary, so we could end up with resources that ultimately build correctly, but fail to
+   * find their class dependencies during rendering in the layout editor.
+   */
+  private void reportResourceTargetShouldDependOnClassTarget(
+      TargetIdeInfo target, TargetMap targetMap, ArtifactLocationDecoder decoder) {
+    Collection<String> missingClasses = logger.getMissingClasses();
+    if (missingClasses == null || missingClasses.isEmpty()) {
+      return;
+    }
+
+    // Sorted entries for deterministic error message.
+    SortedSetMultimap<String, TargetKey> missingClassToTargetMap = TreeMultimap.create();
+
+    SourceToTargetMap sourceToTargetMap = SourceToTargetMap.getInstance(project);
+    ImmutableCollection transitiveDependencies =
+        TransitiveDependencyMap.getInstance(project).getTransitiveDependencies(target.key);
+
+    for (String missingClass : missingClasses) {
+      File sourceFile = getSourceFileForClass(missingClass);
+      if (sourceFile == null) {
+        continue;
+      }
+      ImmutableCollection<TargetKey> sourceTargets =
+          sourceToTargetMap.getRulesForSourceFile(sourceFile);
+      if (sourceTargets
+          .stream()
+          .noneMatch(
+              sourceTarget ->
+                  sourceTarget.equals(target.key)
+                      || transitiveDependencies.contains(sourceTarget))) {
+        missingClassToTargetMap.putAll(missingClass, sourceTargets);
+      }
+    }
+
+    if (missingClassToTargetMap.isEmpty()) {
+      return;
+    }
+
+    HtmlBuilder builder = new HtmlBuilder();
+    addTargetLink(builder, target, decoder)
+        .add(" contains resource files that reference these classes:")
+        .beginList();
+    for (String missingClass : missingClassToTargetMap.keySet()) {
+      builder
+          .listItem()
+          .addLink(missingClass, getLinkManager().createOpenClassUrl(missingClass))
+          .add(" from ");
+      for (TargetKey targetKey : missingClassToTargetMap.get(missingClass)) {
+        addTargetLink(builder, targetMap.get(targetKey), decoder).add(" ");
+      }
+    }
+    builder.endList().add("Please fix your dependencies so that ");
+    addTargetLink(builder, target, decoder)
+        .add(" correctly depends on these classes, ")
+        .addLink("then ", "sync the project", " ", getLinkManager().createSyncProjectUrl())
+        .addLink("and ", "refresh the layout", ".", getLinkManager().createRefreshRenderUrl())
+        .newline()
+        .newline()
+        .addBold(
+            "NOTE: blaze can still build with the incorrect dependencies "
+                + "due to the way it handles resources, "
+                + "but the layout editor needs them to be correct.");
+
+    addIssue()
+        .setSeverity(HighlightSeverity.ERROR, HIGH_PRIORITY + 1) // Reported above missing classes.
+        .setSummary("Missing class dependencies")
+        .setHtmlContent(builder)
+        .build();
+  }
+
+  private File getSourceFileForClass(String className) {
+    return ApplicationManager.getApplication()
+        .runReadAction(
+            (Computable<File>)
+                () -> {
+                  try {
+                    PsiClass psiClass =
+                        JavaPsiFacade.getInstance(project)
+                            .findClass(className, GlobalSearchScope.projectScope(project));
+                    if (psiClass == null) {
+                      return null;
+                    }
+                    return VfsUtilCore.virtualToIoFile(
+                        psiClass.getContainingFile().getVirtualFile());
+                  } catch (IndexNotReadyException ignored) {
+                    // We're in dumb mode. Abort! Abort!
+                    return null;
+                  }
+                });
+  }
+
+  private HtmlBuilder addTargetLink(
+      HtmlBuilder builder, TargetIdeInfo target, ArtifactLocationDecoder decoder) {
+    File buildFile = decoder.decode(target.buildFile);
+    int line =
+        ApplicationManager.getApplication()
+            .runReadAction(
+                (Computable<Integer>)
+                    () -> {
+                      PsiElement buildTargetPsi =
+                          BuildReferenceManager.getInstance(project).resolveLabel(target.key.label);
+                      if (buildTargetPsi == null) {
+                        return -1;
+                      }
+                      return StringUtil.offsetToLineNumber(
+                          buildTargetPsi.getContainingFile().getText(),
+                          buildTargetPsi.getTextOffset());
+                    });
+    String url = HtmlLinkManager.createFilePositionUrl(buildFile, line, 0);
+    if (url != null) {
+      return builder.addLink(target.toString(), url);
+    }
+    return builder.add(target.toString());
+  }
+
+  /** Extension to provide {@link BlazeRenderErrorContributor}. */
+  public static class BlazeProvider extends Provider {
+    @Override
+    public boolean isApplicable(Project project) {
+      return Blaze.isBlazeProject(project);
+    }
+
+    @Override
+    public RenderErrorContributor getContributor(
+        RenderResult result, @Nullable DataContext dataContext) {
+      return new BlazeRenderErrorContributor(result, dataContext);
+    }
+  }
+}
diff --git a/aswb/2.3/src/com/google/idea/blaze/android/run/testrecorder/TestRecorderBlazeCommandRunConfiguration.java b/aswb/2.3/src/com/google/idea/blaze/android/run/testrecorder/TestRecorderBlazeCommandRunConfiguration.java
new file mode 100644
index 0000000..2f6c62e
--- /dev/null
+++ b/aswb/2.3/src/com/google/idea/blaze/android/run/testrecorder/TestRecorderBlazeCommandRunConfiguration.java
@@ -0,0 +1,40 @@
+/*
+ * 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.run.testrecorder;
+
+import com.google.idea.blaze.base.run.BlazeCommandRunConfiguration;
+import com.intellij.openapi.diagnostic.Logger;
+import org.jdom.Element;
+
+/** Blaze integration with test recorder. */
+public class TestRecorderBlazeCommandRunConfiguration extends BlazeCommandRunConfiguration {
+  private static final Logger LOGGER =
+      Logger.getInstance(TestRecorderBlazeCommandRunConfiguration.class);
+
+  public TestRecorderBlazeCommandRunConfiguration(BlazeCommandRunConfiguration baseConfiguration) {
+    super(
+        baseConfiguration.getProject(),
+        baseConfiguration.getFactory(),
+        "TestRecorder" + baseConfiguration.getName());
+    Element element = new Element("toClone");
+    try {
+      baseConfiguration.writeExternal(element);
+      this.readExternal(element);
+    } catch (Exception e) {
+      LOGGER.error(e);
+    }
+  }
+}
diff --git a/aswb/2.3/src/com/google/idea/blaze/android/run/testrecorder/TestRecorderBlazeCommandRunConfigurationProxy.java b/aswb/2.3/src/com/google/idea/blaze/android/run/testrecorder/TestRecorderBlazeCommandRunConfigurationProxy.java
new file mode 100644
index 0000000..b91a720
--- /dev/null
+++ b/aswb/2.3/src/com/google/idea/blaze/android/run/testrecorder/TestRecorderBlazeCommandRunConfigurationProxy.java
@@ -0,0 +1,83 @@
+/*
+ * 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.run.testrecorder;
+
+import com.android.annotations.Nullable;
+import com.android.ddmlib.IDevice;
+import com.google.common.util.concurrent.ListenableFuture;
+import com.google.gct.testrecorder.run.TestRecorderRunConfigurationProxy;
+import com.google.idea.blaze.android.run.binary.BlazeAndroidBinaryRunConfigurationHandler;
+import com.google.idea.blaze.android.run.binary.BlazeAndroidBinaryRunConfigurationState;
+import com.google.idea.blaze.android.run.runner.BlazeAndroidRunConfigurationRunner;
+import com.google.idea.blaze.base.run.BlazeCommandRunConfiguration;
+import com.intellij.execution.configurations.LocatableConfigurationBase;
+import com.intellij.execution.runners.ExecutionEnvironment;
+import com.intellij.openapi.module.Module;
+import java.util.List;
+
+/** {@link TestRecorderRunConfigurationProxy} for blaze. */
+public class TestRecorderBlazeCommandRunConfigurationProxy
+    implements TestRecorderRunConfigurationProxy {
+
+  private final BlazeCommandRunConfiguration myBaseConfiguration;
+  private final BlazeAndroidBinaryRunConfigurationHandler myBaseConfigurationHandler;
+
+  public TestRecorderBlazeCommandRunConfigurationProxy(
+      BlazeCommandRunConfiguration baseConfiguration) {
+    myBaseConfiguration = baseConfiguration;
+    myBaseConfigurationHandler =
+        (BlazeAndroidBinaryRunConfigurationHandler) baseConfiguration.getHandler();
+  }
+
+  @Override
+  public LocatableConfigurationBase getTestRecorderRunConfiguration() {
+    return new TestRecorderBlazeCommandRunConfiguration(myBaseConfiguration);
+  }
+
+  @Override
+  public Module getModule() {
+    return myBaseConfigurationHandler.getModule();
+  }
+
+  @Override
+  public boolean isLaunchActivitySupported() {
+    String mode = myBaseConfigurationHandler.getState().getMode();
+
+    // Supported launch activities are Default and Specified.
+    return BlazeAndroidBinaryRunConfigurationState.LAUNCH_DEFAULT_ACTIVITY.equals(mode)
+        || BlazeAndroidBinaryRunConfigurationState.LAUNCH_SPECIFIC_ACTIVITY.equals(mode);
+  }
+
+  @Override
+  public String getLaunchActivityClass() {
+    BlazeAndroidBinaryRunConfigurationState state = myBaseConfigurationHandler.getState();
+
+    if (BlazeAndroidBinaryRunConfigurationState.LAUNCH_SPECIFIC_ACTIVITY.equals(state.getMode())) {
+      return state.getActivityClass();
+    }
+
+    return "";
+  }
+
+  @Nullable
+  @Override
+  public List<ListenableFuture<IDevice>> getDeviceFutures(ExecutionEnvironment environment) {
+    return environment
+        .getCopyableUserData(BlazeAndroidRunConfigurationRunner.DEVICE_SESSION_KEY)
+        .deviceFutures
+        .get();
+  }
+}
diff --git a/aswb/2.3/src/com/google/idea/blaze/android/run/testrecorder/TestRecorderBlazeCommandRunConfigurationProxyProvider.java b/aswb/2.3/src/com/google/idea/blaze/android/run/testrecorder/TestRecorderBlazeCommandRunConfigurationProxyProvider.java
new file mode 100644
index 0000000..8a2c206
--- /dev/null
+++ b/aswb/2.3/src/com/google/idea/blaze/android/run/testrecorder/TestRecorderBlazeCommandRunConfigurationProxyProvider.java
@@ -0,0 +1,41 @@
+/*
+ * 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.run.testrecorder;
+
+import com.android.annotations.Nullable;
+import com.google.gct.testrecorder.run.TestRecorderRunConfigurationProxy;
+import com.google.gct.testrecorder.run.TestRecorderRunConfigurationProxyProvider;
+import com.google.idea.blaze.android.run.binary.BlazeAndroidBinaryRunConfigurationHandler;
+import com.google.idea.blaze.base.run.BlazeCommandRunConfiguration;
+import com.intellij.execution.configurations.RunConfiguration;
+
+/** Provides {@link TestRecorderBlazeCommandRunConfigurationProxy}. */
+public class TestRecorderBlazeCommandRunConfigurationProxyProvider
+    implements TestRecorderRunConfigurationProxyProvider {
+
+  @Nullable
+  @Override
+  public TestRecorderRunConfigurationProxy getProxy(@Nullable RunConfiguration runConfiguration) {
+    if (runConfiguration instanceof BlazeCommandRunConfiguration
+        && ((BlazeCommandRunConfiguration) runConfiguration).getHandler()
+            instanceof BlazeAndroidBinaryRunConfigurationHandler) {
+      return new TestRecorderBlazeCommandRunConfigurationProxy(
+          (BlazeCommandRunConfiguration) runConfiguration);
+    }
+
+    return null;
+  }
+}
diff --git a/aswb/2.3/src/com/google/idea/blaze/android/settings/BlazeAndroidUserSettingsContributor.java b/aswb/2.3/src/com/google/idea/blaze/android/settings/BlazeAndroidUserSettingsContributor.java
new file mode 100644
index 0000000..3ff141b
--- /dev/null
+++ b/aswb/2.3/src/com/google/idea/blaze/android/settings/BlazeAndroidUserSettingsContributor.java
@@ -0,0 +1,96 @@
+/*
+ * 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.settings;
+
+import com.google.common.base.Objects;
+import com.google.common.collect.ImmutableList;
+import com.google.idea.blaze.android.project.BlazeFeatureEnableService;
+import com.google.idea.blaze.base.settings.ui.BlazeUserSettingsContributor;
+import com.intellij.uiDesigner.core.GridConstraints;
+import javax.swing.JCheckBox;
+import javax.swing.JComponent;
+import javax.swing.JPanel;
+
+/** Contributes Android-specific settings. */
+public class BlazeAndroidUserSettingsContributor implements BlazeUserSettingsContributor {
+  private final ImmutableList<JComponent> components;
+  private JCheckBox useLayoutEditor;
+
+  BlazeAndroidUserSettingsContributor() {
+    useLayoutEditor = new JCheckBox();
+    useLayoutEditor.setSelected(false);
+    useLayoutEditor.setText("Use the layout editor for layout XML files. May freeze IDE.");
+
+    ImmutableList.Builder<JComponent> builder = ImmutableList.builder();
+    if (BlazeFeatureEnableService.isLayoutEditorExperimentEnabled()) {
+      builder.add(useLayoutEditor);
+    }
+    components = builder.build();
+  }
+
+  @Override
+  public void apply() {
+    BlazeAndroidUserSettings settings = BlazeAndroidUserSettings.getInstance();
+    settings.setUseLayoutEditor(useLayoutEditor.isSelected());
+  }
+
+  @Override
+  public void reset() {
+    BlazeAndroidUserSettings settings = BlazeAndroidUserSettings.getInstance();
+    useLayoutEditor.setSelected(settings.getUseLayoutEditor());
+  }
+
+  @Override
+  public boolean isModified() {
+    BlazeAndroidUserSettings settings = BlazeAndroidUserSettings.getInstance();
+    return !Objects.equal(useLayoutEditor.isSelected(), settings.getUseLayoutEditor());
+  }
+
+  @Override
+  public int getRowCount() {
+    return components.size();
+  }
+
+  @Override
+  public int addComponents(JPanel panel, int rowi) {
+    for (JComponent contributedComponent : components) {
+      panel.add(
+          contributedComponent,
+          new GridConstraints(
+              rowi++,
+              0,
+              1,
+              2,
+              GridConstraints.ANCHOR_NORTHWEST,
+              GridConstraints.FILL_NONE,
+              GridConstraints.SIZEPOLICY_CAN_SHRINK | GridConstraints.SIZEPOLICY_CAN_GROW,
+              GridConstraints.SIZEPOLICY_FIXED,
+              null,
+              null,
+              null,
+              0,
+              false));
+    }
+    return rowi;
+  }
+
+  static class BlazeAndroidUserSettingsProvider implements Provider {
+    @Override
+    public BlazeUserSettingsContributor getContributor() {
+      return new BlazeAndroidUserSettingsContributor();
+    }
+  }
+}
diff --git a/aswb/2.3/src/com/google/idea/blaze/android/sync/model/idea/BlazeClassJarProvider.java b/aswb/2.3/src/com/google/idea/blaze/android/sync/model/idea/BlazeClassJarProvider.java
new file mode 100644
index 0000000..15c2add
--- /dev/null
+++ b/aswb/2.3/src/com/google/idea/blaze/android/sync/model/idea/BlazeClassJarProvider.java
@@ -0,0 +1,224 @@
+/*
+ * 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.sync.model.idea;
+
+import com.android.SdkConstants;
+import com.android.tools.idea.model.ClassJarProvider;
+import com.android.tools.idea.res.AppResourceRepository;
+import com.android.tools.idea.res.ResourceClassRegistry;
+import com.google.common.collect.Lists;
+import com.google.idea.blaze.android.sync.model.AndroidResourceModuleRegistry;
+import com.google.idea.blaze.base.ideinfo.AndroidIdeInfo;
+import com.google.idea.blaze.base.ideinfo.JavaIdeInfo;
+import com.google.idea.blaze.base.ideinfo.LibraryArtifact;
+import com.google.idea.blaze.base.ideinfo.TargetIdeInfo;
+import com.google.idea.blaze.base.ideinfo.TargetKey;
+import com.google.idea.blaze.base.ideinfo.TargetMap;
+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.google.idea.blaze.base.targetmaps.TransitiveDependencyMap;
+import com.intellij.openapi.application.ApplicationManager;
+import com.intellij.openapi.application.ModalityState;
+import com.intellij.openapi.module.Module;
+import com.intellij.openapi.project.Project;
+import com.intellij.openapi.vfs.JarFileSystem;
+import com.intellij.openapi.vfs.LocalFileSystem;
+import com.intellij.openapi.vfs.VirtualFile;
+import com.intellij.openapi.vfs.ex.temp.TempFileSystem;
+import com.intellij.util.containers.OrderedSet;
+import java.io.File;
+import java.util.Collection;
+import java.util.List;
+import java.util.concurrent.atomic.AtomicBoolean;
+import org.jetbrains.annotations.Nullable;
+
+/** Collects class jars from the user's build. */
+public class BlazeClassJarProvider extends ClassJarProvider {
+
+  private final Project project;
+  private AtomicBoolean pendingModuleJarsRefresh;
+  private AtomicBoolean pendingDependencyJarsRefresh;
+
+  public BlazeClassJarProvider(final Project project) {
+    this.project = project;
+    this.pendingModuleJarsRefresh = new AtomicBoolean(false);
+    this.pendingDependencyJarsRefresh = new AtomicBoolean(false);
+  }
+
+  @Override
+  @Nullable
+  public VirtualFile findModuleClassFile(String className, Module module) {
+    BlazeProjectData blazeProjectData =
+        BlazeProjectDataManager.getInstance(project).getBlazeProjectData();
+    if (blazeProjectData == null) {
+      return null;
+    }
+
+    ArtifactLocationDecoder decoder = blazeProjectData.artifactLocationDecoder;
+    AndroidResourceModuleRegistry registry = AndroidResourceModuleRegistry.getInstance(project);
+    TargetIdeInfo target = blazeProjectData.targetMap.get(registry.getTargetKey(module));
+
+    if (target == null || target.javaIdeInfo == null) {
+      return null;
+    }
+
+    // As a potential optimization, we could choose an arbitrary android_binary target
+    // that depends on the library to provide a single complete resource jar,
+    // instead of having to rely on dynamic class generation.
+    // TODO: benchmark to see if optimization is worthwhile.
+
+    String classNamePath = className.replace('.', File.separatorChar) + SdkConstants.DOT_CLASS;
+
+    List<File> missingClassJars = Lists.newArrayList();
+    for (LibraryArtifact jar : target.javaIdeInfo.jars) {
+      if (jar.classJar == null) {
+        continue;
+      }
+      File classJarFile = decoder.decode(jar.classJar);
+      VirtualFile classJarVF = findFileByIoFile(classJarFile);
+      if (classJarVF == null) {
+        if (classJarFile.exists()) {
+          missingClassJars.add(classJarFile);
+        }
+        continue;
+      }
+      VirtualFile classFile = findClassInJar(classJarVF, classNamePath);
+      if (classFile != null) {
+        return classFile;
+      }
+    }
+
+    maybeRefreshJars(missingClassJars, pendingModuleJarsRefresh);
+    return null;
+  }
+
+  @Nullable
+  private static VirtualFile findClassInJar(final VirtualFile classJar, String classNamePath) {
+    VirtualFile jarRoot = getJarRootForLocalFile(classJar);
+    if (jarRoot == null) {
+      return null;
+    }
+    return jarRoot.findFileByRelativePath(classNamePath);
+  }
+
+  @Override
+  public List<VirtualFile> getModuleExternalLibraries(Module module) {
+    OrderedSet<VirtualFile> results = new OrderedSet<>();
+    BlazeProjectData blazeProjectData =
+        BlazeProjectDataManager.getInstance(project).getBlazeProjectData();
+
+    if (blazeProjectData == null) {
+      return results;
+    }
+
+    TargetMap targetMap = blazeProjectData.targetMap;
+    ArtifactLocationDecoder decoder = blazeProjectData.artifactLocationDecoder;
+
+    AndroidResourceModuleRegistry registry = AndroidResourceModuleRegistry.getInstance(project);
+    TargetIdeInfo target = targetMap.get(registry.getTargetKey(module));
+
+    if (target == null) {
+      return results;
+    }
+
+    AppResourceRepository repository = AppResourceRepository.getAppResources(module, true);
+
+    List<File> missingClassJars = Lists.newArrayList();
+    for (TargetKey dependencyTargetKey :
+        TransitiveDependencyMap.getInstance(project).getTransitiveDependencies(target.key)) {
+      TargetIdeInfo dependencyTarget = targetMap.get(dependencyTargetKey);
+      if (dependencyTarget == null) {
+        continue;
+      }
+      JavaIdeInfo javaIdeInfo = dependencyTarget.javaIdeInfo;
+      AndroidIdeInfo androidIdeInfo = dependencyTarget.androidIdeInfo;
+
+      // Add all non-resource jars to be searched.
+      // Multiple resource jars will have ID conflicts unless generated dynamically.
+      if (javaIdeInfo != null) {
+        for (LibraryArtifact jar : javaIdeInfo.jars) {
+          if (androidIdeInfo != null && jar.equals(androidIdeInfo.resourceJar)) {
+            // No resource jars.
+            continue;
+          }
+          // Some of these could be empty class jars from resource only android_library targets.
+          // A potential optimization could be to filter out jars like these,
+          // so we don't waste time fetching and searching them.
+          // TODO: benchmark to see if optimization is worthwhile.
+          if (jar.classJar != null) {
+            File classJarFile = decoder.decode(jar.classJar);
+            VirtualFile classJar = findFileByIoFile(classJarFile);
+            if (classJar != null) {
+              results.add(classJar);
+            } else if (classJarFile.exists()) {
+              missingClassJars.add(classJarFile);
+            }
+          }
+        }
+      }
+
+      // Tell ResourceClassRegistry which repository contains our resources and the java packages of
+      // the resources that we're interested in.
+      // When the class loader tries to load a custom view, and the view references resource
+      // classes, layoutlib will ask the class loader for these resource classes.
+      // If these resource classes are in a separate jar from the target (i.e., in a dependency),
+      // then offering their jars will lead to a conflict in the resource IDs.
+      // So instead, the resource class generator will produce dummy resource classes with
+      // non-conflicting IDs to satisfy the class loader.
+      // The resource repository remembers the dynamic IDs that it handed out and when the layoutlib
+      // calls to ask about the name and content of a given resource ID, the repository can just
+      // answer what it has already stored.
+      if (androidIdeInfo != null && repository != null) {
+        ResourceClassRegistry.get(module.getProject())
+            .addLibrary(repository, androidIdeInfo.resourceJavaPackage);
+      }
+    }
+
+    maybeRefreshJars(missingClassJars, pendingDependencyJarsRefresh);
+    return results;
+  }
+
+  private static VirtualFile findFileByIoFile(File file) {
+    return ApplicationManager.getApplication().isUnitTestMode()
+        ? TempFileSystem.getInstance().findFileByIoFile(file)
+        : LocalFileSystem.getInstance().findFileByIoFile(file);
+  }
+
+  private static void maybeRefreshJars(Collection<File> missingJars, AtomicBoolean pendingRefresh) {
+    // We probably need to refresh the virtual file system to find these files, but we can't refresh
+    // here because we're in a read action. We also can't use the async refreshIoFiles since it
+    // still tries to refresh the IO files synchronously. A global async refresh can't find new
+    // files in the ObjFS since we're not watching it.
+    // We need to do our own asynchronous refresh, and guard it with a flag to prevent the event
+    // queue from overflowing.
+    if (!missingJars.isEmpty() && !pendingRefresh.getAndSet(true)) {
+      ApplicationManager.getApplication()
+          .invokeLater(
+              () -> {
+                LocalFileSystem.getInstance().refreshIoFiles(missingJars);
+                pendingRefresh.set(false);
+              },
+              ModalityState.NON_MODAL);
+    }
+  }
+
+  private static VirtualFile getJarRootForLocalFile(VirtualFile file) {
+    return ApplicationManager.getApplication().isUnitTestMode()
+        ? TempFileSystem.getInstance().findFileByPath(file.getPath() + JarFileSystem.JAR_SEPARATOR)
+        : JarFileSystem.getInstance().getJarRootForLocalFile(file);
+  }
+}
diff --git a/aswb/2.3/tests/integrationtests/com/google/idea/blaze/android/sync/model/idea/BlazeClassJarProviderIntegrationTest.java b/aswb/2.3/tests/integrationtests/com/google/idea/blaze/android/sync/model/idea/BlazeClassJarProviderIntegrationTest.java
new file mode 100644
index 0000000..05f1ccf
--- /dev/null
+++ b/aswb/2.3/tests/integrationtests/com/google/idea/blaze/android/sync/model/idea/BlazeClassJarProviderIntegrationTest.java
@@ -0,0 +1,501 @@
+/*
+ * 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.sync.model.idea;
+
+import static com.google.common.truth.Truth.assertThat;
+import static org.mockito.Mockito.mock;
+
+import com.android.tools.idea.model.ClassJarProvider;
+import com.android.tools.idea.res.AppResourceRepository;
+import com.android.tools.idea.res.ResourceClassRegistry;
+import com.google.idea.blaze.android.sync.model.AndroidResourceModule;
+import com.google.idea.blaze.android.sync.model.AndroidResourceModuleRegistry;
+import com.google.idea.blaze.base.BlazeIntegrationTestCase;
+import com.google.idea.blaze.base.ideinfo.AndroidIdeInfo;
+import com.google.idea.blaze.base.ideinfo.ArtifactLocation;
+import com.google.idea.blaze.base.ideinfo.JavaIdeInfo;
+import com.google.idea.blaze.base.ideinfo.LibraryArtifact;
+import com.google.idea.blaze.base.ideinfo.TargetIdeInfo;
+import com.google.idea.blaze.base.ideinfo.TargetKey;
+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.Kind;
+import com.google.idea.blaze.base.model.primitives.Label;
+import com.google.idea.blaze.base.sync.workspace.ArtifactLocationDecoder;
+import com.intellij.facet.FacetManager;
+import com.intellij.facet.ModifiableFacetModel;
+import com.intellij.openapi.application.ApplicationManager;
+import com.intellij.openapi.module.Module;
+import com.intellij.openapi.vfs.VirtualFile;
+import java.io.File;
+import java.io.IOException;
+import java.util.List;
+import org.jetbrains.android.facet.AndroidFacet;
+import org.jetbrains.android.facet.AndroidFacetConfiguration;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** Integration test for {@link BlazeClassJarProvider}. */
+@RunWith(JUnit4.class)
+public class BlazeClassJarProviderIntegrationTest extends BlazeIntegrationTestCase {
+  private static final String BLAZE_BIN = "blaze-out/crosstool/bin";
+
+  private Module module;
+  private ClassJarProvider classJarProvider;
+
+  @Before
+  public void doSetup() {
+    module = testFixture.getModule();
+
+    ArtifactLocationDecoder decoder =
+        (location) -> new File("/src", location.getExecutionRootRelativePath());
+
+    mockBlazeProjectDataManager(
+        new BlazeProjectData(
+            0L, buildTargetMap(), null, null, null, null, decoder, null, null, null));
+    classJarProvider = new BlazeClassJarProvider(getProject());
+  }
+
+  @Test
+  public void testFindModuleClassFile() {
+    createClassesInJars();
+
+    // Make sure we can find classes in the main resource module.
+    assertThat(classJarProvider.findModuleClassFile("com.google.example.main.MainActivity", module))
+        .isEqualTo(
+            fileSystem.findFile(
+                BLAZE_BIN
+                    + "/com/google/example/main.jar!"
+                    + "/com/google/example/main/MainActivity.class"));
+    assertThat(classJarProvider.findModuleClassFile("com.google.example.main.R", module))
+        .isEqualTo(
+            fileSystem.findFile(
+                BLAZE_BIN
+                    + "/com/google/example/main_resources.jar!"
+                    + "/com/google/example/main/R.class"));
+    assertThat(classJarProvider.findModuleClassFile("com.google.example.main.R$string", module))
+        .isEqualTo(
+            fileSystem.findFile(
+                BLAZE_BIN
+                    + "/com/google/example/main_resources.jar!"
+                    + "/com/google/example/main/R$string.class"));
+
+    // And not classes that are missing.
+    assertThat(classJarProvider.findModuleClassFile("com.google.example.main.MissingClass", module))
+        .isNull();
+    assertThat(classJarProvider.findModuleClassFile("com.google.example.main.R$missing", module))
+        .isNull();
+
+    // And not classes in other libraries.
+    assertThat(classJarProvider.findModuleClassFile("com.google.example.java.CustomView", module))
+        .isNull();
+    assertThat(classJarProvider.findModuleClassFile("com.google.example.android_res.R", module))
+        .isNull();
+    assertThat(
+            classJarProvider.findModuleClassFile("com.google.example.android_res.R$style", module))
+        .isNull();
+    assertThat(
+            classJarProvider.findModuleClassFile(
+                "com.google.unrelated.android_res.R$layout", module))
+        .isNull();
+  }
+
+  @Test
+  public void testMissingMainJar() {
+    createClassesInJars();
+
+    ApplicationManager.getApplication()
+        .runWriteAction(
+            () -> {
+              try {
+                // Let's pretend that this hasn't been built yet.
+                fileSystem.findFile(BLAZE_BIN + "/com/google/example/main.jar").delete(this);
+              } catch (IOException ignored) {
+                // ignored
+              }
+            });
+    // This hasn't been built yet, and shouldn't be found.
+    assertThat(classJarProvider.findModuleClassFile("com.google.example.main.MainActivity", module))
+        .isNull();
+    // But these should still be found.
+    assertThat(classJarProvider.findModuleClassFile("com.google.example.main.R", module))
+        .isEqualTo(
+            fileSystem.findFile(
+                BLAZE_BIN
+                    + "/com/google/example/main_resources.jar!"
+                    + "/com/google/example/main/R.class"));
+    assertThat(classJarProvider.findModuleClassFile("com.google.example.main.R$string", module))
+        .isEqualTo(
+            fileSystem.findFile(
+                BLAZE_BIN
+                    + "/com/google/example/main_resources.jar!"
+                    + "/com/google/example/main/R$string.class"));
+  }
+
+  @Test
+  public void testGetModuleExternalLibraries() {
+    // Need AndroidFact for AppResourceRepository.
+    ApplicationManager.getApplication()
+        .runWriteAction(
+            () -> {
+              ModifiableFacetModel model = FacetManager.getInstance(module).createModifiableModel();
+              model.addFacet(new MockAndroidFacet(module));
+              model.commit();
+            });
+
+    List<VirtualFile> externalLibraries = classJarProvider.getModuleExternalLibraries(module);
+    assertThat(externalLibraries)
+        .containsExactly(
+            fileSystem.findFile(BLAZE_BIN + "/com/google/example/android_lib.jar"),
+            fileSystem.findFile(BLAZE_BIN + "/com/google/example/android_res.jar"),
+            fileSystem.findFile(BLAZE_BIN + "/com/google/example/android_res2.jar"),
+            fileSystem.findFile(BLAZE_BIN + "/com/google/example/transitive/android_res.jar"),
+            fileSystem.findFile(BLAZE_BIN + "/com/google/example/java.jar"),
+            fileSystem.findFile(BLAZE_BIN + "/com/google/example/transitive/java.jar"),
+            fileSystem.findFile(BLAZE_BIN + "/com/google/example/shared/java.jar"),
+            fileSystem.findFile(BLAZE_BIN + "/com/google/example/shared2/java.jar"),
+            fileSystem.findFile("com/google/example/import.jar"),
+            fileSystem.findFile("com/google/example/transitive/import.jar"),
+            fileSystem.findFile("com/google/example/transitive/import2.jar"));
+
+    // Make sure we can generate dynamic classes from all resource packages in dependencies.
+    ResourceClassRegistry registry = ResourceClassRegistry.get(getProject());
+    AppResourceRepository repository = AppResourceRepository.getAppResources(module, false);
+    assertThat(repository).isNotNull();
+    assertThat(registry.findClassDefinition("com.google.example.android_res.R", repository))
+        .isNotNull();
+    assertThat(registry.findClassDefinition("com.google.example.android_res.R$string", repository))
+        .isNotNull();
+    assertThat(registry.findClassDefinition("com.google.example.android_res2.R", repository))
+        .isNotNull();
+    assertThat(registry.findClassDefinition("com.google.example.android_res2.R$layout", repository))
+        .isNotNull();
+    assertThat(
+            registry.findClassDefinition("com.google.example.transitive.android_res.R", repository))
+        .isNotNull();
+    assertThat(
+            registry.findClassDefinition(
+                "com.google.example.transitive.android_res.R$style", repository))
+        .isNotNull();
+
+    // And nothing else.
+    assertThat(registry.findClassDefinition("com.google.example.main.MainActivity", repository))
+        .isNull();
+    assertThat(registry.findClassDefinition("com.google.example.android_res.Bogus", repository))
+        .isNull();
+    assertThat(registry.findClassDefinition("com.google.example.main.R", repository)).isNull();
+    assertThat(registry.findClassDefinition("com.google.example.main.R$string", repository))
+        .isNull();
+    assertThat(registry.findClassDefinition("com.google.example.java.CustomView", repository))
+        .isNull();
+    assertThat(registry.findClassDefinition("com.google.unrelated.android_res.R", repository))
+        .isNull();
+    assertThat(
+            registry.findClassDefinition("com.google.unrelated.android_res.R$layout", repository))
+        .isNull();
+  }
+
+  @Test
+  public void testMissingExternalJars() {
+    ApplicationManager.getApplication()
+        .runWriteAction(
+            () -> {
+              try {
+                // Let's pretend that these haven't been built yet.
+                fileSystem.findFile(BLAZE_BIN + "/com/google/example/java.jar").delete(this);
+                fileSystem
+                    .findFile(BLAZE_BIN + "/com/google/example/android_res2.jar")
+                    .delete(this);
+                fileSystem
+                    .findFile(BLAZE_BIN + "/com/google/example/shared2/java.jar")
+                    .delete(this);
+              } catch (IOException ignored) {
+                // ignored
+              }
+            });
+    List<VirtualFile> externalLibraries = classJarProvider.getModuleExternalLibraries(module);
+    assertThat(externalLibraries)
+        .containsExactly(
+            fileSystem.findFile(BLAZE_BIN + "/com/google/example/android_lib.jar"),
+            fileSystem.findFile(BLAZE_BIN + "/com/google/example/android_res.jar"),
+            // This should be missing.
+            // fileSystem.findFile(BLAZE_BIN + "/com/google/example/android_res2.jar"),
+            fileSystem.findFile(BLAZE_BIN + "/com/google/example/transitive/android_res.jar"),
+            // This should be missing.
+            // fileSystem.findFile(BLAZE_BIN + "/com/google/example/java.jar"),
+            fileSystem.findFile(BLAZE_BIN + "/com/google/example/transitive/java.jar"),
+            fileSystem.findFile(BLAZE_BIN + "/com/google/example/shared/java.jar"),
+            // This should be missing.
+            // fileSystem.findFile(BLAZE_BIN + "/com/google/example/shared2/java.jar"),
+            fileSystem.findFile("com/google/example/import.jar"),
+            fileSystem.findFile("com/google/example/transitive/import.jar"),
+            fileSystem.findFile("com/google/example/transitive/import2.jar"));
+  }
+
+  private void createClassesInJars() {
+    fileSystem.createFile(
+        BLAZE_BIN
+            + "/com/google/example/main.jar!"
+            + "/com/google/example/main/MainActivity.class");
+    fileSystem.createFile(
+        BLAZE_BIN + "/com/google/example/main_resources.jar!" + "/com/google/example/main/R.class");
+    fileSystem.createFile(
+        BLAZE_BIN
+            + "/com/google/example/main_resources.jar!"
+            + "/com/google/example/main/R$string.class");
+    fileSystem.createFile(
+        BLAZE_BIN + "/com/google/example/java.jar!" + "/com/google/example/java/CustomView.class");
+    fileSystem.createFile(
+        BLAZE_BIN
+            + "/com/google/example/android_res_resources.jar!"
+            + "/com/google/example/android_res/R.class");
+    fileSystem.createFile(
+        BLAZE_BIN
+            + "/com/google/example/android_res_resources.jar!"
+            + "/com/google/example/android_res/R$style.class");
+    fileSystem.createFile(
+        BLAZE_BIN
+            + "/com/google/unrelated/android_res_resources.jar!"
+            + "/com/google/unrelated/android_res/R$layout.class");
+  }
+
+  private TargetMap buildTargetMap() {
+    Label mainResourceLibrary = new Label("//com/google/example:main");
+    Label androidLibraryDependency = new Label("//com/google/example:android_lib");
+    Label androidResourceDependency = new Label("//com/google/example:android_res");
+    Label androidResourceDependency2 = new Label("//com/google/example:android_res2");
+    Label transitiveResourceDependency = new Label("//com/google/example/transitive:android_res");
+    Label javaDependency = new Label("//com/google/example:java");
+    Label transitiveJavaDependency = new Label("//com/google/example/transitive:java");
+    Label sharedJavaDependency = new Label("//com/google/example/shared:java");
+    Label sharedJavaDependency2 = new Label("//com/google/example/shared2:java");
+    Label importDependency = new Label("//com/google/example:import");
+    Label transitiveImportDependency = new Label("//com/google/example/transitive:import");
+    Label unrelatedJava = new Label("//com/google/unrelated:java");
+    Label unrelatedAndroidLibrary = new Label("//com/google/unrelated:android_lib");
+    Label unrelatedAndroidResource = new Label("//com/google/unrelated:android_res");
+
+    AndroidResourceModuleRegistry registry = new AndroidResourceModuleRegistry();
+    registry.put(
+        module,
+        AndroidResourceModule.builder(TargetKey.forPlainTarget(mainResourceLibrary)).build());
+    // Not using these, but they should be in the registry.
+    registry.put(
+        mock(Module.class),
+        AndroidResourceModule.builder(TargetKey.forPlainTarget(androidResourceDependency)).build());
+    registry.put(
+        mock(Module.class),
+        AndroidResourceModule.builder(TargetKey.forPlainTarget(androidResourceDependency2))
+            .build());
+    registry.put(
+        mock(Module.class),
+        AndroidResourceModule.builder(TargetKey.forPlainTarget(transitiveResourceDependency))
+            .build());
+    registry.put(
+        mock(Module.class),
+        AndroidResourceModule.builder(TargetKey.forPlainTarget(unrelatedAndroidResource)).build());
+    registerProjectService(AndroidResourceModuleRegistry.class, registry);
+
+    return TargetMapBuilder.builder()
+        .addTarget(
+            TargetIdeInfo.builder()
+                .setLabel(mainResourceLibrary)
+                .setKind(Kind.ANDROID_LIBRARY)
+                .setJavaInfo(
+                    javaInfoWithJars(
+                        "com/google/example/main.jar", "com/google/example/main_resources.jar"))
+                .setAndroidInfo(
+                    androidInfoWithResourceAndJar(
+                        "com.google.example.main",
+                        "com/google/example/main/res",
+                        "com/google/example/main_resources.jar"))
+                .addDependency(androidLibraryDependency)
+                .addDependency(androidResourceDependency)
+                .addDependency(androidResourceDependency2)
+                .addDependency(javaDependency)
+                .addDependency(importDependency))
+        .addTarget(
+            TargetIdeInfo.builder()
+                .setLabel(androidLibraryDependency)
+                .setKind(Kind.ANDROID_LIBRARY)
+                .setJavaInfo(javaInfoWithJars("com/google/example/android_lib.jar"))
+                .addDependency(transitiveResourceDependency))
+        .addTarget(
+            TargetIdeInfo.builder()
+                .setLabel(androidResourceDependency)
+                .setKind(Kind.ANDROID_LIBRARY)
+                .setJavaInfo(
+                    javaInfoWithJars(
+                        "com/google/example/android_res.jar",
+                        "com/google/example/android_res_resources.jar"))
+                .setAndroidInfo(
+                    androidInfoWithResourceAndJar(
+                        "com.google.example.android_res",
+                        "com/google/example/android_res/res",
+                        "com/google/example/android_res_resources.jar")))
+        .addTarget(
+            TargetIdeInfo.builder()
+                .setLabel(androidResourceDependency2)
+                .setKind(Kind.ANDROID_LIBRARY)
+                .setJavaInfo(
+                    javaInfoWithJars(
+                        "com/google/example/android_res2.jar",
+                        "com/google/example/android_res2_resources.jar"))
+                .setAndroidInfo(
+                    androidInfoWithResourceAndJar(
+                        "com.google.example.android_res2",
+                        "com/google/example/android_res2/res",
+                        "com/google/example/android_res2_resources.jar")))
+        .addTarget(
+            TargetIdeInfo.builder()
+                .setLabel(transitiveResourceDependency)
+                .setKind(Kind.ANDROID_LIBRARY)
+                .setJavaInfo(
+                    javaInfoWithJars(
+                        "com/google/example/transitive/android_res.jar",
+                        "com/google/example/transitive/android_res_resources.jar"))
+                .setAndroidInfo(
+                    androidInfoWithResourceAndJar(
+                        "com.google.example.transitive.android_res",
+                        "com/google/example/transitive/android_res/res",
+                        "com/google/example/transitive/android_res_resources.jar")))
+        .addTarget(
+            TargetIdeInfo.builder()
+                .setLabel(javaDependency)
+                .setKind(Kind.JAVA_LIBRARY)
+                .setJavaInfo(javaInfoWithJars("com/google/example/java.jar"))
+                .addDependency(transitiveJavaDependency)
+                .addDependency(sharedJavaDependency)
+                .addDependency(sharedJavaDependency2)
+                .addDependency(transitiveImportDependency))
+        .addTarget(
+            TargetIdeInfo.builder()
+                .setLabel(transitiveJavaDependency)
+                .setKind(Kind.JAVA_LIBRARY)
+                .setJavaInfo(javaInfoWithJars("com/google/example/transitive/java.jar"))
+                .addDependency(sharedJavaDependency)
+                .addDependency(sharedJavaDependency2))
+        .addTarget(
+            TargetIdeInfo.builder()
+                .setLabel(sharedJavaDependency)
+                .setKind(Kind.JAVA_LIBRARY)
+                .setJavaInfo(javaInfoWithJars("com/google/example/shared/java.jar"))
+                .addDependency(sharedJavaDependency2))
+        .addTarget(
+            TargetIdeInfo.builder()
+                .setLabel(sharedJavaDependency2)
+                .setKind(Kind.JAVA_LIBRARY)
+                .setJavaInfo(javaInfoWithJars("com/google/example/shared2/java.jar")))
+        .addTarget(
+            TargetIdeInfo.builder()
+                .setLabel(importDependency)
+                .setKind(Kind.JAVA_IMPORT)
+                .setJavaInfo(javaInfoWithCheckedInJars("com/google/example/import.jar")))
+        .addTarget(
+            TargetIdeInfo.builder()
+                .setLabel(transitiveImportDependency)
+                .setKind(Kind.JAVA_IMPORT)
+                .setJavaInfo(
+                    javaInfoWithCheckedInJars(
+                        "com/google/example/transitive/import.jar",
+                        "com/google/example/transitive/import2.jar")))
+        .addTarget(
+            TargetIdeInfo.builder()
+                .setLabel(unrelatedJava)
+                .setKind(Kind.JAVA_LIBRARY)
+                .setJavaInfo(javaInfoWithJars("com/google/unrelated/java.jar")))
+        .addTarget(
+            TargetIdeInfo.builder()
+                .setLabel(unrelatedAndroidLibrary)
+                .setKind(Kind.ANDROID_LIBRARY)
+                .setJavaInfo(javaInfoWithJars("com/google/unrelated/android_lib.jar")))
+        .addTarget(
+            TargetIdeInfo.builder()
+                .setLabel(unrelatedAndroidResource)
+                .setKind(Kind.ANDROID_LIBRARY)
+                .setJavaInfo(
+                    javaInfoWithJars(
+                        "com/google/unrelated/android_res.jar",
+                        "com/google/unrelated/android_res_resources.jar"))
+                .setAndroidInfo(
+                    androidInfoWithResourceAndJar(
+                        "com.google.unrelated.android_res",
+                        "com/google/unrelated/android_res/res",
+                        "com/google/unrelated/android_res_resources.jar")))
+        .build();
+  }
+
+  private JavaIdeInfo.Builder javaInfoWithJars(String... relativeJarPaths) {
+    JavaIdeInfo.Builder builder = JavaIdeInfo.builder();
+    for (String relativeJarPath : relativeJarPaths) {
+      ArtifactLocation jar =
+          ArtifactLocation.builder()
+              .setRootExecutionPathFragment(BLAZE_BIN)
+              .setRelativePath(relativeJarPath)
+              .setIsSource(false)
+              .build();
+      builder.addJar(LibraryArtifact.builder().setClassJar(jar));
+      fileSystem.createFile(jar.getExecutionRootRelativePath());
+    }
+    return builder;
+  }
+
+  private JavaIdeInfo.Builder javaInfoWithCheckedInJars(String... relativeJarPaths) {
+    JavaIdeInfo.Builder builder = JavaIdeInfo.builder();
+    for (String relativeJarPath : relativeJarPaths) {
+      ArtifactLocation jar =
+          ArtifactLocation.builder().setRelativePath(relativeJarPath).setIsSource(true).build();
+      builder.addJar(LibraryArtifact.builder().setClassJar(jar));
+      fileSystem.createFile(jar.getExecutionRootRelativePath());
+    }
+    return builder;
+  }
+
+  private static AndroidIdeInfo.Builder androidInfoWithResourceAndJar(
+      String javaPackage, String relativeResourcePath, String relativeJarPath) {
+    return AndroidIdeInfo.builder()
+        .setGenerateResourceClass(true)
+        .setResourceJavaPackage(javaPackage)
+        .addResource(
+            ArtifactLocation.builder()
+                .setRelativePath(relativeResourcePath)
+                .setIsSource(true)
+                .build())
+        .setResourceJar(
+            LibraryArtifact.builder()
+                .setClassJar(
+                    // No need to createFile for this one since it should also be in the Java info.
+                    ArtifactLocation.builder()
+                        .setRootExecutionPathFragment(BLAZE_BIN)
+                        .setRelativePath(relativeJarPath)
+                        .setIsSource(false)
+                        .build()));
+  }
+
+  private static class MockAndroidFacet extends AndroidFacet {
+    public MockAndroidFacet(Module module) {
+      super(module, AndroidFacet.NAME, new AndroidFacetConfiguration());
+    }
+
+    @Override
+    public void initFacet() {
+      // We don't need this, but it causes trouble when it tries looking for project templates.
+    }
+  }
+}
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
new file mode 100755
index 0000000..72541b3
--- /dev/null
+++ b/aswb/2.3/tests/unittests/com/google/idea/blaze/android/project/BlazeBuildSystemServiceTest.java
@@ -0,0 +1,263 @@
+/*
+ * 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.project;
+
+import static com.google.common.truth.Truth.assertThat;
+import static org.mockito.Matchers.eq;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyNoMoreInteractions;
+import static org.mockito.Mockito.when;
+
+import com.android.tools.idea.project.BuildSystemService;
+import com.google.common.collect.Maps;
+import com.google.idea.blaze.android.sync.model.AndroidResourceModule;
+import com.google.idea.blaze.android.sync.model.AndroidResourceModuleRegistry;
+import com.google.idea.blaze.base.BlazeTestCase;
+import com.google.idea.blaze.base.actions.BlazeBuildService;
+import com.google.idea.blaze.base.ideinfo.ArtifactLocation;
+import com.google.idea.blaze.base.ideinfo.TargetIdeInfo;
+import com.google.idea.blaze.base.ideinfo.TargetKey;
+import com.google.idea.blaze.base.ideinfo.TargetMap;
+import com.google.idea.blaze.base.ideinfo.TargetMapBuilder;
+import com.google.idea.blaze.base.lang.buildfile.references.BuildReferenceManager;
+import com.google.idea.blaze.base.model.BlazeProjectData;
+import com.google.idea.blaze.base.model.primitives.Label;
+import com.google.idea.blaze.base.projectview.ProjectViewManager;
+import com.google.idea.blaze.base.projectview.ProjectViewSet;
+import com.google.idea.blaze.base.scope.BlazeContext;
+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.BlazeSyncManager;
+import com.google.idea.blaze.base.sync.BlazeSyncPlugin.ModuleEditor;
+import com.google.idea.blaze.base.sync.data.BlazeProjectDataManager;
+import com.google.idea.blaze.base.sync.workspace.ArtifactLocationDecoder;
+import com.google.idea.blaze.base.sync.workspace.WorkspacePathResolver;
+import com.intellij.mock.MockModule;
+import com.intellij.mock.MockVirtualFile;
+import com.intellij.openapi.editor.LazyRangeMarkerFactory;
+import com.intellij.openapi.editor.impl.LazyRangeMarkerFactoryImpl;
+import com.intellij.openapi.extensions.ExtensionPoint;
+import com.intellij.openapi.extensions.ExtensionPointName;
+import com.intellij.openapi.fileEditor.FileEditorManager;
+import com.intellij.openapi.fileEditor.OpenFileDescriptor;
+import com.intellij.openapi.module.Module;
+import com.intellij.openapi.vfs.VirtualFile;
+import com.intellij.openapi.vfs.ex.temp.TempFileSystem;
+import com.intellij.psi.PsiElement;
+import com.intellij.psi.PsiFile;
+import java.io.File;
+import java.util.Map;
+import javax.annotation.Nullable;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+import org.mockito.ArgumentCaptor;
+
+/** Test cases for {@link BlazeBuildSystemService}. */
+@RunWith(JUnit4.class)
+public class BlazeBuildSystemServiceTest extends BlazeTestCase {
+  Module module;
+  BuildSystemService service;
+
+  @Override
+  protected void initTest(Container applicationServices, Container projectServices) {
+    module = new MockModule(project, () -> {});
+
+    mockBlazeImportSettings(projectServices); // For Blaze.isBlazeProject.
+    createMocksForBuildProject(applicationServices);
+    createMocksForSyncProject(projectServices);
+    createMocksForAddDependency(applicationServices, projectServices);
+
+    ExtensionPoint<BuildSystemService> extensionPoint =
+        registerExtensionPoint(
+            ExtensionPointName.create("com.android.project.buildSystemService"),
+            BuildSystemService.class);
+    extensionPoint.registerExtension(new BlazeBuildSystemService());
+
+    service = BuildSystemService.getInstance(project);
+  }
+
+  @Test
+  public void testIsBlazeBuildSystemService() {
+    assertThat(service).isInstanceOf(BlazeBuildSystemService.class);
+  }
+
+  @Test
+  public void testBuildProject() {
+    service.buildProject(project);
+    verify(BlazeBuildService.getInstance()).buildProject(project);
+    verifyNoMoreInteractions(BlazeBuildService.getInstance());
+  }
+
+  @Test
+  public void testSyncProject() {
+    service.syncProject(project);
+    verify(BlazeSyncManager.getInstance(project)).incrementalProjectSync();
+    verifyNoMoreInteractions(BlazeSyncManager.getInstance(project));
+  }
+
+  @Test
+  public void testAddDependencyWithBuildTargetPsi() throws Exception {
+    PsiElement buildTargetPsi = mock(PsiElement.class);
+    PsiFile psiFile = mock(PsiFile.class);
+
+    BuildReferenceManager buildReferenceManager = BuildReferenceManager.getInstance(project);
+    when(buildReferenceManager.resolveLabel(new Label("//foo:bar"))).thenReturn(buildTargetPsi);
+    when(buildTargetPsi.getContainingFile()).thenReturn(psiFile);
+    when(buildTargetPsi.getTextOffset()).thenReturn(1337);
+
+    VirtualFile buildFile = TempFileSystem.getInstance().findFileByPath("/foo/BUILD");
+    assertThat(buildFile).isNotNull();
+    when(psiFile.getVirtualFile()).thenReturn(buildFile);
+
+    String dependency = "com.android.foo:bar"; // Doesn't matter.
+
+    service.addDependency(module, dependency);
+
+    ArgumentCaptor<OpenFileDescriptor> descriptorCaptor =
+        ArgumentCaptor.forClass(OpenFileDescriptor.class);
+    verify(FileEditorManager.getInstance(project))
+        .openTextEditor(descriptorCaptor.capture(), eq(true));
+    OpenFileDescriptor descriptor = descriptorCaptor.getValue();
+    assertThat(descriptor.getProject()).isEqualTo(project);
+    assertThat(descriptor.getFile()).isEqualTo(buildFile);
+    assertThat(descriptor.getOffset()).isEqualTo(1337);
+    verifyNoMoreInteractions(FileEditorManager.getInstance(project));
+  }
+
+  @Test
+  public void testAddDependencyWithoutBuildTargetPsi() throws Exception {
+    // Can't find PSI for the target.
+    when(BuildReferenceManager.getInstance(project).resolveLabel(new Label("//foo:bar")))
+        .thenReturn(null);
+
+    VirtualFile buildFile = TempFileSystem.getInstance().findFileByPath("/foo/BUILD");
+    assertThat(buildFile).isNotNull();
+
+    String dependency = "com.android.foo:bar"; // Doesn't matter.
+
+    service.addDependency(module, dependency);
+
+    verify(FileEditorManager.getInstance(project)).openFile(buildFile, true);
+    verifyNoMoreInteractions(FileEditorManager.getInstance(project));
+  }
+
+  private void mockBlazeImportSettings(Container projectServices) {
+    BlazeImportSettingsManager importSettingsManager = new BlazeImportSettingsManager(project);
+    importSettingsManager.setImportSettings(
+        new BlazeImportSettings("", "", "", "", "", Blaze.BuildSystem.Blaze));
+    projectServices.register(BlazeImportSettingsManager.class, importSettingsManager);
+  }
+
+  private static void createMocksForBuildProject(Container applicationServices) {
+    applicationServices.register(BlazeBuildService.class, mock(BlazeBuildService.class));
+  }
+
+  private static void createMocksForSyncProject(Container projectServices) {
+    projectServices.register(ProjectViewManager.class, new MockProjectViewManager());
+    projectServices.register(BlazeSyncManager.class, mock(BlazeSyncManager.class));
+  }
+
+  private void createMocksForAddDependency(
+      Container applicationServices, Container projectServices) {
+    projectServices.register(BlazeProjectDataManager.class, new MockProjectDataManager());
+    projectServices.register(FileEditorManager.class, mock(FileEditorManager.class));
+    projectServices.register(BuildReferenceManager.class, mock(BuildReferenceManager.class));
+    projectServices.register(LazyRangeMarkerFactory.class, mock(LazyRangeMarkerFactoryImpl.class));
+
+    applicationServices.register(TempFileSystem.class, new MockFileSystem("/foo/BUILD"));
+
+    AndroidResourceModuleRegistry moduleRegistry = new AndroidResourceModuleRegistry();
+    moduleRegistry.put(
+        module,
+        AndroidResourceModule.builder(TargetKey.forPlainTarget(new Label("//foo:bar"))).build());
+    projectServices.register(AndroidResourceModuleRegistry.class, moduleRegistry);
+  }
+
+  private static class MockProjectViewManager extends ProjectViewManager {
+    private ProjectViewSet viewSet;
+
+    public MockProjectViewManager() {
+      this.viewSet = ProjectViewSet.builder().build();
+    }
+
+    @Nullable
+    @Override
+    public ProjectViewSet getProjectViewSet() {
+      return viewSet;
+    }
+
+    @Nullable
+    @Override
+    public ProjectViewSet reloadProjectView(
+        BlazeContext context, WorkspacePathResolver workspacePathResolver) {
+      return viewSet;
+    }
+  }
+
+  private static class MockProjectDataManager implements BlazeProjectDataManager {
+    private BlazeProjectData projectData;
+
+    public MockProjectDataManager() {
+      TargetMap targetMap =
+          TargetMapBuilder.builder()
+              .addTarget(
+                  TargetIdeInfo.builder()
+                      .setLabel(new Label("//foo:bar"))
+                      .setBuildFile(ArtifactLocation.builder().setRelativePath("foo/BUILD").build())
+                      .build())
+              .build();
+      ArtifactLocationDecoder decoder = (location) -> new File("/", location.getRelativePath());
+
+      projectData =
+          new BlazeProjectData(0L, targetMap, null, null, null, null, decoder, null, null, null);
+    }
+
+    @Nullable
+    @Override
+    public BlazeProjectData getBlazeProjectData() {
+      return projectData;
+    }
+
+    @Override
+    public ModuleEditor editModules() {
+      return null;
+    }
+  }
+
+  private static class MockFileSystem extends TempFileSystem {
+    private Map<String, VirtualFile> files;
+
+    public MockFileSystem(String... paths) {
+      files = Maps.newHashMap();
+      for (String path : paths) {
+        files.put(path, new MockVirtualFile(path));
+      }
+    }
+
+    @Override
+    public VirtualFile findFileByPath(String path) {
+      return files.get(path);
+    }
+
+    @Override
+    public VirtualFile findFileByIoFile(File file) {
+      return findFileByPath(file.getPath());
+    }
+  }
+}
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
new file mode 100644
index 0000000..e4fe775
--- /dev/null
+++ b/aswb/2.3/tests/unittests/com/google/idea/blaze/android/project/BlazeFeatureEnabledServiceTest.java
@@ -0,0 +1,158 @@
+/*
+ * 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.project;
+
+import static com.google.common.truth.Truth.THROW_ASSERTION_ERROR;
+import static com.google.common.truth.Truth.assertThat;
+import static org.mockito.Mockito.mock;
+
+import com.android.tools.idea.project.FeatureEnableService;
+import com.google.common.collect.ImmutableList;
+import com.google.idea.blaze.android.settings.BlazeAndroidUserSettings;
+import com.google.idea.blaze.base.BlazeTestCase;
+import com.google.idea.blaze.base.model.BlazeProjectData;
+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.BlazeSyncPlugin.ModuleEditor;
+import com.google.idea.blaze.base.sync.data.BlazeProjectDataManager;
+import com.google.idea.common.experiments.ExperimentService;
+import com.intellij.openapi.extensions.ExtensionPoint;
+import com.intellij.openapi.extensions.ExtensionPointName;
+import javax.annotation.Nullable;
+import org.jetbrains.annotations.NotNull;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** Test cases for {@link BlazeFeatureEnableService}. */
+@RunWith(JUnit4.class)
+public class BlazeFeatureEnabledServiceTest extends BlazeTestCase {
+  private MockExperimentService experimentService;
+  private BlazeAndroidUserSettings userSettings;
+  private MockBlazeProjectDataManager projectDataManager;
+  private FeatureEnableService featureEnableService;
+
+  @Override
+  protected void initTest(
+      @NotNull Container applicationServices, @NotNull Container projectServices) {
+    super.initTest(applicationServices, projectServices);
+    experimentService = new MockExperimentService();
+    applicationServices.register(ExperimentService.class, experimentService);
+
+    userSettings = new BlazeAndroidUserSettings();
+    applicationServices.register(BlazeAndroidUserSettings.class, userSettings);
+
+    projectDataManager = new MockBlazeProjectDataManager();
+    projectServices.register(BlazeProjectDataManager.class, projectDataManager);
+
+    BlazeImportSettingsManager importSettingsManager = new BlazeImportSettingsManager(project);
+    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(
+            ExtensionPointName.create("com.android.project.featureEnableService"),
+            FeatureEnableService.class);
+    extensionPoint.registerExtension(new BlazeFeatureEnableService());
+
+    featureEnableService = FeatureEnableService.getInstance(project);
+  }
+
+  @Test
+  public void testIsBlazeFeatureEnableService() {
+    assertThat(featureEnableService).isInstanceOf(BlazeFeatureEnableService.class);
+  }
+
+  @Test
+  public void testIsNotBlazeFeatureEnableService() {
+    BlazeImportSettingsManager.getInstance(project).loadState(null);
+    assertThat(FeatureEnableService.getInstance(project))
+        .isNotInstanceOf(BlazeFeatureEnableService.class);
+  }
+
+  @Test
+  public void testGetLayoutEditorEnabled() {
+    for (boolean experimentEnabled : ImmutableList.of(true, false)) {
+      for (boolean settingEnabled : ImmutableList.of(true, false)) {
+        for (boolean projectDataReady : ImmutableList.of(true, false)) {
+          userSettings.setUseLayoutEditor(settingEnabled);
+          experimentService.setEnableLayoutEditor(experimentEnabled);
+          projectDataManager.setBlazeProjectData(
+              projectDataReady ? mock(BlazeProjectData.class) : null);
+          assertThat(featureEnableService.isLayoutEditorEnabled(project))
+              .isEqualTo(settingEnabled && experimentEnabled && projectDataReady);
+        }
+      }
+    }
+  }
+
+  private static class MockBlazeProjectDataManager implements BlazeProjectDataManager {
+    private BlazeProjectData blazeProjectData;
+
+    public void setBlazeProjectData(BlazeProjectData blazeProjectData) {
+      this.blazeProjectData = blazeProjectData;
+    }
+
+    @Nullable
+    @Override
+    public BlazeProjectData getBlazeProjectData() {
+      return blazeProjectData;
+    }
+
+    @Override
+    public ModuleEditor editModules() {
+      return null;
+    }
+  }
+
+  private static class MockExperimentService implements ExperimentService {
+    private boolean enableLayoutEditor;
+
+    public void setEnableLayoutEditor(boolean enableLayoutEditor) {
+      this.enableLayoutEditor = enableLayoutEditor;
+    }
+
+    @Override
+    public boolean getExperiment(String key, boolean defaultValue) {
+      assertThat(key).isEqualTo("enable.layout.editor");
+      return enableLayoutEditor;
+    }
+
+    @Nullable
+    @Override
+    public String getExperimentString(String key, @Nullable String defaultValue) {
+      THROW_ASSERTION_ERROR.fail("Should not be called.");
+      return null;
+    }
+
+    @Override
+    public int getExperimentInt(String key, int defaultValue) {
+      THROW_ASSERTION_ERROR.fail("Should not be called.");
+      return 0;
+    }
+
+    @Override
+    public void startExperimentScope() {}
+
+    @Override
+    public void endExperimentScope() {}
+  }
+}
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
new file mode 100644
index 0000000..5bbeec6
--- /dev/null
+++ b/aswb/2.3/tests/unittests/com/google/idea/blaze/android/rendering/BlazeRenderErrorContributorTest.java
@@ -0,0 +1,768 @@
+/*
+ * 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.rendering;
+
+import static com.google.common.truth.Truth.assertThat;
+import static org.mockito.Mockito.mock;
+
+import com.android.tools.idea.rendering.RenderErrorContributor;
+import com.android.tools.idea.rendering.RenderErrorModelFactory;
+import com.android.tools.idea.rendering.RenderResult;
+import com.android.tools.idea.rendering.errors.ui.RenderErrorModel;
+import com.google.common.collect.ImmutableCollection;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.Iterables;
+import com.google.idea.blaze.android.sync.model.AndroidResourceModule;
+import com.google.idea.blaze.android.sync.model.AndroidResourceModuleRegistry;
+import com.google.idea.blaze.base.BlazeTestCase;
+import com.google.idea.blaze.base.ideinfo.AndroidIdeInfo;
+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.TargetKey;
+import com.google.idea.blaze.base.ideinfo.TargetMap;
+import com.google.idea.blaze.base.ideinfo.TargetMapBuilder;
+import com.google.idea.blaze.base.lang.buildfile.references.BuildReferenceManager;
+import com.google.idea.blaze.base.model.BlazeProjectData;
+import com.google.idea.blaze.base.model.primitives.Label;
+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.BlazeSyncPlugin.ModuleEditor;
+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;
+import com.google.idea.blaze.base.targetmaps.TransitiveDependencyMap;
+import com.intellij.mock.MockModule;
+import com.intellij.mock.MockPsiFile;
+import com.intellij.mock.MockPsiManager;
+import com.intellij.mock.MockVirtualFile;
+import com.intellij.openapi.extensions.ExtensionPoint;
+import com.intellij.openapi.extensions.ExtensionPointName;
+import com.intellij.openapi.fileTypes.FileTypeManager;
+import com.intellij.openapi.fileTypes.MockFileTypeManager;
+import com.intellij.openapi.module.Module;
+import com.intellij.openapi.module.ModuleUtilCore;
+import com.intellij.openapi.project.Project;
+import com.intellij.openapi.vfs.VfsUtilCore;
+import com.intellij.openapi.vfs.VirtualFile;
+import com.intellij.psi.JavaPsiFacade;
+import com.intellij.psi.PsiClass;
+import com.intellij.psi.PsiElement;
+import com.intellij.psi.PsiFile;
+import com.intellij.psi.PsiManager;
+import com.intellij.psi.impl.JavaPsiFacadeImpl;
+import com.intellij.psi.search.GlobalSearchScope;
+import com.intellij.psi.search.ProjectScopeBuilder;
+import com.intellij.psi.search.ProjectScopeBuilderImpl;
+import com.intellij.psi.util.PsiUtil.NullPsiClass;
+import java.io.File;
+import java.util.stream.Collectors;
+import javax.annotation.Nullable;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** Unit tests for {@link BlazeRenderErrorContributor}. */
+@RunWith(JUnit4.class)
+public class BlazeRenderErrorContributorTest extends BlazeTestCase {
+  private static final String BLAZE_BIN = "blaze-out/crosstool/bin";
+  private static final String GENERATED_RESOURCES_ERROR = "Generated resources";
+  private static final String NON_STANDARD_MANIFEST_NAME_ERROR = "Non-standard manifest name";
+  private static final String MISSING_CLASS_DEPENDENCIES_ERROR = "Missing class dependencies";
+
+  private Module module;
+  private MockBlazeProjectDataManager projectDataManager;
+  private BlazeRenderErrorContributor.BlazeProvider provider;
+
+  @Override
+  protected void initTest(Container applicationServices, Container projectServices) {
+    super.initTest(applicationServices, projectServices);
+    applicationServices.register(FileTypeManager.class, new MockFileTypeManager());
+
+    projectServices.register(BuildReferenceManager.class, new MockBuildReferenceManager(project));
+    projectServices.register(TransitiveDependencyMap.class, new TransitiveDependencyMap(project));
+    projectServices.register(ProjectScopeBuilder.class, new ProjectScopeBuilderImpl(project));
+    projectServices.register(
+        AndroidResourceModuleRegistry.class, new AndroidResourceModuleRegistry());
+
+    BlazeImportSettingsManager importSettingsManager = new BlazeImportSettingsManager(project);
+    BlazeImportSettings settings = new BlazeImportSettings("", "", "", "", "", BuildSystem.Blaze);
+    importSettingsManager.setImportSettings(settings);
+    projectServices.register(BlazeImportSettingsManager.class, importSettingsManager);
+    projectServices.register(
+        BlazeImportSettingsManagerLegacy.class, new BlazeImportSettingsManagerLegacy(project));
+
+    createPsiClassesAndSourceToTargetMap(projectServices);
+
+    projectDataManager = new MockBlazeProjectDataManager();
+    projectServices.register(BlazeProjectDataManager.class, projectDataManager);
+
+    ExtensionPoint<RenderErrorContributor.Provider> extensionPoint =
+        registerExtensionPoint(
+            ExtensionPointName.create("com.android.rendering.renderErrorContributor"),
+            RenderErrorContributor.Provider.class);
+    extensionPoint.registerExtension(new RenderErrorContributor.Provider());
+    extensionPoint.registerExtension(new BlazeRenderErrorContributor.BlazeProvider());
+
+    module = new MockModule(project, () -> {});
+
+    // For the isApplicable tests.
+    provider = new BlazeRenderErrorContributor.BlazeProvider();
+  }
+
+  @Test
+  public void testProviderIsApplicable() {
+    assertThat(provider.isApplicable(project)).isTrue();
+  }
+
+  @Test
+  public void testProviderNotApplicableIfNotBlaze() {
+    BlazeImportSettingsManager.getInstance(project).loadState(null);
+    assertThat(provider.isApplicable(project)).isFalse();
+  }
+
+  @Test
+  public void testNoIssuesIfNoErrors() {
+    PsiFile file = new MockPsiFile(new MockPsiManager(project));
+    file.putUserData(ModuleUtilCore.KEY_MODULE, module);
+    RenderResult result = RenderResult.createBlank(file);
+    RenderErrorModel errorModel = RenderErrorModelFactory.createErrorModel(result, null);
+    assertThat(errorModel.getIssues()).isEmpty();
+  }
+
+  @Test
+  public void testNoBlazeIssuesIfNoRelatedErrors() {
+    RenderErrorModel errorModel = createRenderErrorModelWithBrokenClasses();
+    errorModel
+        .getIssues()
+        .forEach(
+            issue ->
+                assertThat(issue.getSummary())
+                    .isNoneOf(
+                        GENERATED_RESOURCES_ERROR,
+                        NON_STANDARD_MANIFEST_NAME_ERROR,
+                        MISSING_CLASS_DEPENDENCIES_ERROR));
+  }
+
+  @Test
+  public void testReportGeneratedResources() {
+    createTargetMapWithGeneratedResources();
+    RenderErrorModel errorModel = createRenderErrorModelWithBrokenClasses();
+
+    RenderErrorModel.Issue generatedResourcesIssue =
+        Iterables.getOnlyElement(
+            errorModel
+                .getIssues()
+                .stream()
+                .filter(issue -> issue.getSummary().equals(GENERATED_RESOURCES_ERROR))
+                .collect(Collectors.toList()));
+
+    assertThat(generatedResourcesIssue.getHtmlContent())
+        .isEqualTo(
+            "Generated resources will not be discovered by the IDE:"
+                + "<DL>"
+                + "<DD>-&NBSP;"
+                + "com/google/example/dependency/generated/res "
+                + "from <A HREF=\"file:///src/com/google/example/dependency/BUILD\">"
+                + "//com/google/example:generated</A>"
+                + "<DD>-&NBSP;"
+                + "com/google/example/main/generated/res "
+                + "from <A HREF=\"file:///src/com/google/example/main/BUILD\">"
+                + "//com/google/example:main</A>"
+                + "<DD>-&NBSP;"
+                + "com/google/example/transitive/generated/one/res "
+                + "from <A HREF=\"file:///src/com/google/example/transitive/BUILD\">"
+                + "//com/google/example/transitive:generated</A>"
+                + "<DD>-&NBSP;"
+                + "com/google/example/transitive/generated/two/res "
+                + "from <A HREF=\"file:///src/com/google/example/transitive/BUILD\">"
+                + "//com/google/example/transitive:generated</A>"
+                + "</DL>"
+                + "Please avoid using generated resources, then "
+                + "<A HREF=\"action:sync\">sync the project</A> and "
+                + "<A HREF=\"refreshRender\">refresh the layout</A>.");
+  }
+
+  @Test
+  public void testReportNonStandardAndroidManifestName() {
+    createTargetMapWithNonStandardAndroidManifestName();
+    RenderErrorModel errorModel = createRenderErrorModelWithBrokenClasses();
+
+    RenderErrorModel.Issue nonStandardManifestNameIssue =
+        Iterables.getOnlyElement(
+            errorModel
+                .getIssues()
+                .stream()
+                .filter(issue -> issue.getSummary().equals(NON_STANDARD_MANIFEST_NAME_ERROR))
+                .collect(Collectors.toList()));
+
+    assertThat(nonStandardManifestNameIssue.getHtmlContent())
+        .isEqualTo(
+            "<A HREF=\"file:///src/com/google/example/main/BUILD\">"
+                + "//com/google/example:main</A> "
+                + "uses a non-standard name for the Android manifest: "
+                + "<A HREF=\"file:///src/com/google/example/main/WeirdManifest.xml\">"
+                + "WeirdManifest.xml</A>"
+                + "<BR/>"
+                + "Please rename it to AndroidManifest.xml, then "
+                + "<A HREF=\"action:sync\">sync the project</A> and "
+                + "<A HREF=\"refreshRender\">refresh the layout</A>.");
+  }
+
+  @Test
+  public void testNoReportNonStandardAndroidManifestNameInDependency() {
+    createTargetMapWithNonStandardAndroidManifestNameInDependency();
+    RenderErrorModel errorModel = createRenderErrorModelWithBrokenClasses();
+
+    errorModel
+        .getIssues()
+        .forEach(
+            issue -> assertThat(issue.getSummary()).isNotEqualTo(NON_STANDARD_MANIFEST_NAME_ERROR));
+  }
+
+  @Test
+  public void testReportMissingClassDependencies() {
+    createTargetMapWithMissingClassDependency();
+    RenderErrorModel errorModel =
+        createRenderErrorModelWithMissingClasses(
+            "com.google.example.independent.LibraryView",
+            "com.google.example.independent.LibraryView2",
+            "com.google.example.independent.Library2View",
+            "com.google.example.dependent.LibraryView",
+            "com.google.example.ResourceView");
+
+    RenderErrorModel.Issue missingClassDependenciesIssue =
+        Iterables.getOnlyElement(
+            errorModel
+                .getIssues()
+                .stream()
+                .filter(issue -> issue.getSummary().equals(MISSING_CLASS_DEPENDENCIES_ERROR))
+                .collect(Collectors.toList()));
+
+    assertThat(missingClassDependenciesIssue.getHtmlContent())
+        .isEqualTo(
+            "<A HREF=\"file:///src/com/google/example/BUILD\">"
+                + "//com/google/example:resources</A> "
+                + "contains resource files that reference these classes:"
+                + "<DL>"
+                + "<DD>-&NBSP;"
+                + "<A HREF=\"openClass:com.google.example.independent.Library2View\">"
+                + "com.google.example.independent.Library2View</A> "
+                + "from <A HREF=\"file:///src/com/google/example/BUILD\">"
+                + "//com/google/example/independent:library2</A> "
+                + "<DD>-&NBSP;"
+                + "<A HREF=\"openClass:com.google.example.independent.LibraryView\">"
+                + "com.google.example.independent.LibraryView</A> "
+                + "from <A HREF=\"file:///src/com/google/example/BUILD\">"
+                + "//com/google/example/independent:library</A> "
+                + "<DD>-&NBSP;"
+                + "<A HREF=\"openClass:com.google.example.independent.LibraryView2\">"
+                + "com.google.example.independent.LibraryView2</A> "
+                + "from <A HREF=\"file:///src/com/google/example/BUILD\">"
+                + "//com/google/example/independent:library</A> "
+                + "</DL>"
+                + "Please fix your dependencies so that "
+                + "<A HREF=\"file:///src/com/google/example/BUILD\">"
+                + "//com/google/example:resources</A> "
+                + "correctly depends on these classes, then "
+                + "<A HREF=\"action:sync\">sync the project</A> and "
+                + "<A HREF=\"refreshRender\">refresh the layout</A>."
+                + "<BR/>"
+                + "<BR/>"
+                + "<B>NOTE: blaze can still build with the incorrect dependencies "
+                + "due to the way it handles resources, "
+                + "but the layout editor needs them to be correct.</B>");
+  }
+
+  @Test
+  public void testNoReportMissingClassDependenciesIfClassInSameTarget() {
+    createTargetMapWithMissingClassDependency();
+    RenderErrorModel errorModel =
+        createRenderErrorModelWithMissingClasses("com.google.example.ResourceView");
+
+    errorModel
+        .getIssues()
+        .forEach(
+            issue -> assertThat(issue.getSummary()).isNotEqualTo(MISSING_CLASS_DEPENDENCIES_ERROR));
+  }
+
+  @Test
+  public void testNoReportMissingClassDependenciesIfClassInDependency() {
+    createTargetMapWithMissingClassDependency();
+    RenderErrorModel errorModel =
+        createRenderErrorModelWithMissingClasses("com.google.example.dependent.LibraryView");
+
+    errorModel
+        .getIssues()
+        .forEach(
+            issue -> assertThat(issue.getSummary()).isNotEqualTo(MISSING_CLASS_DEPENDENCIES_ERROR));
+  }
+
+  private RenderErrorModel createRenderErrorModelWithBrokenClasses() {
+    PsiFile file = new MockPsiFile(new MockPsiManager(project));
+    file.putUserData(ModuleUtilCore.KEY_MODULE, module);
+    RenderResult result = RenderResult.createBlank(file);
+    result
+        .getLogger()
+        .addBrokenClass("com.google.example.CustomView", new Exception("resource not found"));
+    return RenderErrorModelFactory.createErrorModel(result, null);
+  }
+
+  private RenderErrorModel createRenderErrorModelWithMissingClasses(String... classNames) {
+    PsiFile file = new MockPsiFile(new MockPsiManager(project));
+    file.putUserData(ModuleUtilCore.KEY_MODULE, module);
+    RenderResult result = RenderResult.createBlank(file);
+    for (String className : classNames) {
+      result.getLogger().addMissingClass(className);
+    }
+    return RenderErrorModelFactory.createErrorModel(result, null);
+  }
+
+  private static ArtifactLocation artifact(String relativePath, boolean isSource) {
+    return ArtifactLocation.builder()
+        .setIsSource(isSource)
+        .setRootExecutionPathFragment(isSource ? "" : BLAZE_BIN)
+        .setRelativePath(relativePath)
+        .build();
+  }
+
+  private void createTargetMapWithGeneratedResources() {
+    Label mainResourcesTarget = new Label("//com/google/example:main");
+    Label dependencyGeneratedResourceTarget = new Label("//com/google/example:generated");
+    Label dependencySourceResourceTarget = new Label("//com/google/example:source");
+    Label transitiveGeneratedResourcesTarget =
+        new Label("//com/google/example/transitive:generated");
+    Label transitiveSourceResourceTarget = new Label("//com/google/example/transitive:source");
+    Label unrelatedGeneratedResourceTarget = new Label("//com/google/unrelated:generated");
+    Label unrelatedSourceResourceTarget = new Label("//com/google/unrelated:source");
+
+    ArtifactLocation mainGeneratedResource =
+        artifact("com/google/example/main/generated/res", false);
+    ArtifactLocation mainSourceResource = artifact("com/google/example/main/source/res", true);
+    ArtifactLocation dependencyGeneratedResource =
+        artifact("com/google/example/dependency/generated/res", false);
+    ArtifactLocation dependencySourceResource =
+        artifact("com/google/example/dependency/source/res", true);
+    ArtifactLocation transitiveGeneratedResourceOne =
+        artifact("com/google/example/transitive/generated/one/res", false);
+    ArtifactLocation transitiveGeneratedResourceTwo =
+        artifact("com/google/example/transitive/generated/two/res", false);
+    ArtifactLocation transitiveSourceResource =
+        artifact("com/google/example/transitive/source/res", true);
+    ArtifactLocation unrelatedGeneratedResource =
+        artifact("com/google/unrelated/generated/res", false);
+    ArtifactLocation unrelatedSourceResource = artifact("com/google/unrelated/source/res", true);
+
+    ArtifactLocation mainBuildFile = artifact("com/google/example/main/BUILD", true);
+    ArtifactLocation dependencyBuildFile = artifact("com/google/example/dependency/BUILD", true);
+    ArtifactLocation transitiveBuildFile = artifact("com/google/example/transitive/BUILD", true);
+    ArtifactLocation unrelatedBuildFile = artifact("com/google/unrelated/BUILD", true);
+
+    AndroidResourceModuleRegistry registry = AndroidResourceModuleRegistry.getInstance(project);
+    registry.put(
+        module,
+        AndroidResourceModule.builder(TargetKey.forPlainTarget(mainResourcesTarget))
+            // .addResource(mainGeneratedResource) // Dropped.
+            .addResource(mainSourceResource)
+            .addTransitiveResourceDependency(dependencyGeneratedResourceTarget)
+            .addTransitiveResource(dependencyGeneratedResource)
+            .addTransitiveResourceDependency(dependencySourceResourceTarget)
+            .addTransitiveResource(dependencySourceResource)
+            .addTransitiveResourceDependency(transitiveGeneratedResourcesTarget)
+            .addTransitiveResource(transitiveGeneratedResourceOne)
+            .addTransitiveResource(transitiveGeneratedResourceTwo)
+            .addTransitiveResourceDependency(transitiveSourceResourceTarget)
+            .addTransitiveResource(transitiveSourceResource)
+            .build());
+    // Not using these, but they should be in the registry.
+    registry.put(
+        mock(Module.class),
+        AndroidResourceModule.builder(TargetKey.forPlainTarget(dependencyGeneratedResourceTarget))
+            // .addResource(dependencyGeneratedResource) // Dropped.
+            .addTransitiveResourceDependency(transitiveSourceResourceTarget)
+            .addTransitiveResource(transitiveSourceResource)
+            .build());
+    registry.put(
+        mock(Module.class),
+        AndroidResourceModule.builder(TargetKey.forPlainTarget(dependencySourceResourceTarget))
+            .addResource(dependencySourceResource)
+            .addTransitiveResourceDependency(transitiveGeneratedResourcesTarget)
+            .addTransitiveResource(transitiveGeneratedResourceOne)
+            .addTransitiveResource(transitiveGeneratedResourceTwo)
+            .build());
+    registry.put(
+        mock(Module.class),
+        AndroidResourceModule.builder(TargetKey.forPlainTarget(transitiveGeneratedResourcesTarget))
+            // .addResource(transitiveGeneratedResourceOne) // Dropped.
+            // .addResource(transitiveGeneratedResourceTwo) // Dropped.
+            .build());
+    registry.put(
+        mock(Module.class),
+        AndroidResourceModule.builder(TargetKey.forPlainTarget(transitiveSourceResourceTarget))
+            .addResource(transitiveSourceResource)
+            .build());
+    registry.put(
+        mock(Module.class),
+        AndroidResourceModule.builder(TargetKey.forPlainTarget(unrelatedGeneratedResourceTarget))
+            // .addResource(unrelatedGeneratedResource) // Dropped.
+            .build());
+    registry.put(
+        mock(Module.class),
+        AndroidResourceModule.builder(TargetKey.forPlainTarget(unrelatedSourceResourceTarget))
+            .addResource(unrelatedSourceResource)
+            .build());
+
+    TargetMap targetMap =
+        TargetMapBuilder.builder()
+            .addTarget(
+                TargetIdeInfo.builder()
+                    .setLabel(mainResourcesTarget)
+                    .setBuildFile(mainBuildFile)
+                    .setAndroidInfo(
+                        AndroidIdeInfo.builder()
+                            .setGenerateResourceClass(true)
+                            .addResource(mainGeneratedResource)
+                            .addResource(mainSourceResource))
+                    .addDependency(dependencyGeneratedResourceTarget)
+                    .addDependency(dependencySourceResourceTarget))
+            .addTarget(
+                TargetIdeInfo.builder()
+                    .setLabel(dependencyGeneratedResourceTarget)
+                    .setBuildFile(dependencyBuildFile)
+                    .setAndroidInfo(
+                        AndroidIdeInfo.builder()
+                            .setGenerateResourceClass(true)
+                            .addResource(dependencyGeneratedResource))
+                    .addDependency(transitiveSourceResourceTarget))
+            .addTarget(
+                TargetIdeInfo.builder()
+                    .setLabel(dependencySourceResourceTarget)
+                    .setBuildFile(dependencyBuildFile)
+                    .setAndroidInfo(
+                        AndroidIdeInfo.builder()
+                            .setGenerateResourceClass(true)
+                            .addResource(dependencySourceResource))
+                    .addDependency(transitiveGeneratedResourcesTarget))
+            .addTarget(
+                TargetIdeInfo.builder()
+                    .setLabel(transitiveGeneratedResourcesTarget)
+                    .setBuildFile(transitiveBuildFile)
+                    .setAndroidInfo(
+                        AndroidIdeInfo.builder()
+                            .setGenerateResourceClass(true)
+                            .addResource(transitiveGeneratedResourceOne)
+                            .addResource(transitiveGeneratedResourceTwo)))
+            .addTarget(
+                TargetIdeInfo.builder()
+                    .setLabel(transitiveSourceResourceTarget)
+                    .setBuildFile(transitiveBuildFile)
+                    .setAndroidInfo(
+                        AndroidIdeInfo.builder()
+                            .setGenerateResourceClass(true)
+                            .addResource(transitiveSourceResource)))
+            .addTarget(
+                TargetIdeInfo.builder()
+                    .setLabel(unrelatedGeneratedResourceTarget)
+                    .setBuildFile(unrelatedBuildFile)
+                    .setAndroidInfo(
+                        AndroidIdeInfo.builder()
+                            .setGenerateResourceClass(true)
+                            .addResource(unrelatedGeneratedResource)))
+            .addTarget(
+                TargetIdeInfo.builder()
+                    .setLabel(unrelatedSourceResourceTarget)
+                    .setBuildFile(unrelatedBuildFile)
+                    .setAndroidInfo(
+                        AndroidIdeInfo.builder()
+                            .setGenerateResourceClass(true)
+                            .addResource(unrelatedSourceResource)))
+            .build();
+
+    projectDataManager.setTargetMap(targetMap);
+  }
+
+  private void createTargetMapWithNonStandardAndroidManifestName() {
+    Label mainResourceTarget = new Label("//com/google/example:main");
+
+    ArtifactLocation mainManifest = artifact("com/google/example/main/WeirdManifest.xml", true);
+    ArtifactLocation mainResource = artifact("com/google/example/main/res", true);
+    ArtifactLocation mainBuildFile = artifact("com/google/example/main/BUILD", true);
+
+    AndroidResourceModuleRegistry registry = AndroidResourceModuleRegistry.getInstance(project);
+    registry.put(
+        module,
+        AndroidResourceModule.builder(TargetKey.forPlainTarget(mainResourceTarget))
+            .addResource(mainResource)
+            .build());
+
+    TargetMap targetMap =
+        TargetMapBuilder.builder()
+            .addTarget(
+                TargetIdeInfo.builder()
+                    .setLabel(mainResourceTarget)
+                    .setBuildFile(mainBuildFile)
+                    .setAndroidInfo(
+                        AndroidIdeInfo.builder()
+                            .setGenerateResourceClass(true)
+                            .setManifestFile(mainManifest)
+                            .addResource(mainResource)))
+            .build();
+
+    projectDataManager.setTargetMap(targetMap);
+  }
+
+  private void createTargetMapWithNonStandardAndroidManifestNameInDependency() {
+    Label mainResourceTarget = new Label("//com/google/example:main");
+    Label dependencyResourceTarget = new Label("//com/google/example:dependency");
+
+    ArtifactLocation mainManifest = artifact("com/google/example/main/AndroidManifest.xml", true);
+    ArtifactLocation mainResource = artifact("com/google/example/main/res", true);
+    ArtifactLocation mainBuildFile = artifact("com/google/example/main/BUILD", true);
+
+    ArtifactLocation dependencyManifest =
+        artifact("com/google/example/dependency/MyManifest.xml", true);
+    ArtifactLocation dependencyResource = artifact("com/google/example/dependency/res", true);
+    ArtifactLocation dependencyBuildFile = artifact("com/google/example/dependency/BUILD", true);
+
+    AndroidResourceModuleRegistry registry = AndroidResourceModuleRegistry.getInstance(project);
+    registry.put(
+        module,
+        AndroidResourceModule.builder(TargetKey.forPlainTarget(mainResourceTarget))
+            .addResource(mainResource)
+            .addTransitiveResourceDependency(dependencyResourceTarget)
+            .addTransitiveResource(dependencyResource)
+            .build());
+    registry.put(
+        mock(Module.class),
+        AndroidResourceModule.builder(TargetKey.forPlainTarget(dependencyResourceTarget))
+            .addResource(dependencyResource)
+            .build());
+
+    TargetMap targetMap =
+        TargetMapBuilder.builder()
+            .addTarget(
+                TargetIdeInfo.builder()
+                    .setLabel(mainResourceTarget)
+                    .setBuildFile(mainBuildFile)
+                    .setAndroidInfo(
+                        AndroidIdeInfo.builder()
+                            .setGenerateResourceClass(true)
+                            .setManifestFile(mainManifest)
+                            .addResource(mainResource)))
+            .addTarget(
+                TargetIdeInfo.builder()
+                    .setLabel(dependencyResourceTarget)
+                    .setBuildFile(dependencyBuildFile)
+                    .setAndroidInfo(
+                        AndroidIdeInfo.builder()
+                            .setGenerateResourceClass(true)
+                            .setManifestFile(dependencyManifest)
+                            .addResource(dependencyResource)))
+            .build();
+
+    projectDataManager.setTargetMap(targetMap);
+  }
+
+  private void createTargetMapWithMissingClassDependency() {
+    Label parentTarget = new Label("//com/google/example:app");
+    Label independentLibraryTarget = new Label("//com/google/example/independent:library");
+    Label independentLibrary2Target = new Label("//com/google/example/independent:library2");
+    Label dependentLibraryTarget = new Label("//com/google/example/dependent:library");
+    Label resourcesTarget = new Label("//com/google/example:resources");
+
+    ArtifactLocation manifest = artifact("com/google/example/AndroidManifest.xml", true);
+    ArtifactLocation resources = artifact("com/google/example/res", true);
+    ArtifactLocation buildFile = artifact("com/google/example/BUILD", true);
+
+    AndroidResourceModuleRegistry registry = AndroidResourceModuleRegistry.getInstance(project);
+    registry.put(
+        module,
+        AndroidResourceModule.builder(TargetKey.forPlainTarget(resourcesTarget))
+            .addResource(resources)
+            .build());
+
+    TargetMap targetMap =
+        TargetMapBuilder.builder()
+            .addTarget(
+                TargetIdeInfo.builder()
+                    .setLabel(parentTarget)
+                    .setBuildFile(buildFile)
+                    .addDependency(independentLibraryTarget)
+                    .addDependency(resourcesTarget))
+            .addTarget(
+                TargetIdeInfo.builder()
+                    .setLabel(independentLibraryTarget)
+                    .setBuildFile(buildFile)
+                    .setJavaInfo(JavaIdeInfo.builder())
+                    .addDependency(independentLibrary2Target))
+            .addTarget(
+                TargetIdeInfo.builder()
+                    .setLabel(independentLibrary2Target)
+                    .setBuildFile(buildFile)
+                    .setJavaInfo(JavaIdeInfo.builder()))
+            .addTarget(
+                TargetIdeInfo.builder()
+                    .setLabel(resourcesTarget)
+                    .setBuildFile(buildFile)
+                    .setAndroidInfo(
+                        AndroidIdeInfo.builder()
+                            .setGenerateResourceClass(true)
+                            .setManifestFile(manifest)
+                            .addResource(resources))
+                    .addDependency(dependentLibraryTarget))
+            .addTarget(
+                TargetIdeInfo.builder()
+                    .setLabel(dependentLibraryTarget)
+                    .setBuildFile(buildFile)
+                    .setJavaInfo(JavaIdeInfo.builder()))
+            .build();
+
+    projectDataManager.setTargetMap(targetMap);
+  }
+
+  private void createPsiClassesAndSourceToTargetMap(Container projectServices) {
+    PsiManager psiManager = new MockPsiManager(project);
+
+    VirtualFile independentLibraryView =
+        new MockVirtualFile("src/com/google/example/independent/LibraryView.java");
+    VirtualFile independentLibraryView2 =
+        new MockVirtualFile("src/com/google/example/independent/LibraryView2.java");
+    VirtualFile independentLibrary2View =
+        new MockVirtualFile("src/com/google/example/independent/Library2View.java");
+    VirtualFile dependentLibraryView =
+        new MockVirtualFile("src/com/google/example/dependent/LibraryView.java");
+    VirtualFile resourceView = new MockVirtualFile("src/com/google/example/ResourceView.java");
+
+    ImmutableMap<String, PsiClass> classes =
+        ImmutableMap.of(
+            "com.google.example.independent.LibraryView",
+            new MockPsiClass(psiManager, independentLibraryView),
+            "com.google.example.independent.LibraryView2",
+            new MockPsiClass(psiManager, independentLibraryView2),
+            "com.google.example.independent.Library2View",
+            new MockPsiClass(psiManager, independentLibrary2View),
+            "com.google.example.dependent.LibraryView",
+            new MockPsiClass(psiManager, dependentLibraryView),
+            "com.google.example.ResourceView",
+            new MockPsiClass(psiManager, resourceView));
+
+    ImmutableMap<File, TargetKey> sourceToTarget =
+        ImmutableMap.of(
+            VfsUtilCore.virtualToIoFile(independentLibraryView),
+            TargetKey.forPlainTarget(new Label("//com/google/example/independent:library")),
+            VfsUtilCore.virtualToIoFile(independentLibraryView2),
+            TargetKey.forPlainTarget(new Label("//com/google/example/independent:library")),
+            VfsUtilCore.virtualToIoFile(independentLibrary2View),
+            TargetKey.forPlainTarget(new Label("//com/google/example/independent:library2")),
+            VfsUtilCore.virtualToIoFile(dependentLibraryView),
+            TargetKey.forPlainTarget(new Label("//com/google/example/dependent:library")),
+            VfsUtilCore.virtualToIoFile(resourceView),
+            TargetKey.forPlainTarget(new Label("//com/google/example:resources")));
+
+    projectServices.register(
+        JavaPsiFacade.class, new MockJavaPsiFacade(project, psiManager, classes));
+    projectServices.register(SourceToTargetMap.class, new MockSourceToTargetMap(sourceToTarget));
+  }
+
+  private static class MockBlazeProjectDataManager implements BlazeProjectDataManager {
+    private BlazeProjectData blazeProjectData;
+
+    public void setTargetMap(TargetMap targetMap) {
+      ArtifactLocationDecoder decoder =
+          (location) -> new File("/src", location.getExecutionRootRelativePath());
+      this.blazeProjectData =
+          new BlazeProjectData(0L, targetMap, null, null, null, null, decoder, null, null, null);
+    }
+
+    @Nullable
+    @Override
+    public BlazeProjectData getBlazeProjectData() {
+      return blazeProjectData;
+    }
+
+    @Override
+    public ModuleEditor editModules() {
+      return null;
+    }
+  }
+
+  private static class MockBuildReferenceManager extends BuildReferenceManager {
+    public MockBuildReferenceManager(Project project) {
+      super(project);
+    }
+
+    @Nullable
+    @Override
+    public PsiElement resolveLabel(Label label) {
+      return null;
+    }
+  }
+
+  private static class MockPsiClass extends NullPsiClass {
+    private PsiFile psiFile;
+
+    public MockPsiClass(PsiManager psiManager, VirtualFile virtualFile) {
+      psiFile =
+          new MockPsiFile(psiManager) {
+            @Override
+            public VirtualFile getVirtualFile() {
+              return virtualFile;
+            }
+          };
+    }
+
+    @Override
+    public PsiFile getContainingFile() {
+      return psiFile;
+    }
+  }
+
+  private static class MockJavaPsiFacade extends JavaPsiFacadeImpl {
+    private ImmutableMap<String, PsiClass> classes;
+
+    public MockJavaPsiFacade(
+        Project project, PsiManager psiManager, ImmutableMap<String, PsiClass> classes) {
+      super(project, psiManager, null, null);
+      this.classes = classes;
+    }
+
+    @Override
+    public PsiClass findClass(String qualifiedName, GlobalSearchScope scope) {
+      return classes.get(qualifiedName);
+    }
+  }
+
+  private static class MockSourceToTargetMap implements SourceToTargetMap {
+    private ImmutableMap<File, TargetKey> sourceToTarget;
+
+    public MockSourceToTargetMap(ImmutableMap<File, TargetKey> sourceToTarget) {
+      this.sourceToTarget = sourceToTarget;
+    }
+
+    @Override
+    public ImmutableCollection<Label> getTargetsToBuildForSourceFile(File file) {
+      return ImmutableList.of();
+    }
+
+    @Override
+    public ImmutableCollection<TargetKey> getRulesForSourceFile(File file) {
+      return ImmutableList.of(sourceToTarget.get(file));
+    }
+  }
+}
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
new file mode 100644
index 0000000..86c20cf
--- /dev/null
+++ b/aswb/2.3/tests/unittests/com/google/idea/blaze/android/run/testrecorder/BlazeConfigurationsTest.java
@@ -0,0 +1,292 @@
+/*
+ * 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.run.testrecorder;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.android.tools.idea.run.editor.AndroidDebugger;
+import com.android.tools.idea.run.editor.AndroidJavaDebugger;
+import com.android.tools.idea.run.editor.DeployTargetProvider;
+import com.android.tools.idea.run.editor.ShowChooserTargetProvider;
+import com.google.common.base.Predicate;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+import com.google.gct.testrecorder.run.TestRecorderRunConfigurationProxy;
+import com.google.gct.testrecorder.run.TestRecorderRunConfigurationProxyProvider;
+import com.google.gct.testrecorder.ui.TestRecorderAction;
+import com.google.idea.blaze.android.run.binary.BlazeAndroidBinaryRunConfigurationHandler;
+import com.google.idea.blaze.android.run.binary.BlazeAndroidBinaryRunConfigurationHandlerProvider;
+import com.google.idea.blaze.android.run.binary.BlazeAndroidBinaryRunConfigurationState;
+import com.google.idea.blaze.base.BlazeTestCase;
+import com.google.idea.blaze.base.bazel.BuildSystemProvider;
+import com.google.idea.blaze.base.bazel.WorkspaceRootProvider;
+import com.google.idea.blaze.base.ideinfo.TargetIdeInfo;
+import com.google.idea.blaze.base.lang.buildfile.language.semantics.RuleDefinition;
+import com.google.idea.blaze.base.model.BlazeVersionData;
+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.WorkspaceRoot;
+import com.google.idea.blaze.base.run.BlazeBeforeRunTaskProvider;
+import com.google.idea.blaze.base.run.BlazeCommandRunConfiguration;
+import com.google.idea.blaze.base.run.BlazeCommandRunConfigurationType;
+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.targetfinder.TargetFinder;
+import com.google.idea.blaze.base.settings.Blaze;
+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.intellij.execution.BeforeRunTaskProvider;
+import com.intellij.execution.RunManager;
+import com.intellij.execution.RunManagerEx;
+import com.intellij.execution.configurations.ConfigurationType;
+import com.intellij.execution.configurations.RunConfiguration;
+import com.intellij.execution.impl.RunManagerImpl;
+import com.intellij.ide.util.ProjectPropertiesComponentImpl;
+import com.intellij.mock.MockModule;
+import com.intellij.mock.MockProject;
+import com.intellij.openapi.extensions.ExtensionPoint;
+import com.intellij.openapi.extensions.ExtensionPointName;
+import com.intellij.openapi.extensions.Extensions;
+import com.intellij.openapi.extensions.impl.ExtensionPointImpl;
+import com.intellij.openapi.extensions.impl.ExtensionsAreaImpl;
+import com.intellij.openapi.fileTypes.FileNameMatcher;
+import com.intellij.openapi.module.Module;
+import com.intellij.openapi.project.Project;
+import java.io.File;
+import java.util.List;
+import javax.annotation.Nullable;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** Test cases for {@link TestRecorderBlazeCommandRunConfiguration}. */
+@RunWith(JUnit4.class)
+public class BlazeConfigurationsTest extends BlazeTestCase {
+
+  @Override
+  protected void initTest(Container applicationServices, Container projectServices) {
+    super.initTest(applicationServices, projectServices);
+
+    mockBlazeImportSettings(projectServices);
+    applicationServices.register(TargetFinder.class, new MockTargetFinder());
+
+    ExtensionPoint<ConfigurationType> configurationTypeExtensionPoint =
+        registerExtensionPoint(ConfigurationType.CONFIGURATION_TYPE_EP, ConfigurationType.class);
+    configurationTypeExtensionPoint.registerExtension(new BlazeCommandRunConfigurationType());
+
+    ExtensionPoint<BlazeCommandRunConfigurationHandlerProvider> handlerProviderExtensionPoint =
+        registerExtensionPoint(
+            BlazeCommandRunConfigurationHandlerProvider.EP_NAME,
+            BlazeCommandRunConfigurationHandlerProvider.class);
+    handlerProviderExtensionPoint.registerExtension(
+        new MockBlazeAndroidBinaryRunConfigurationHandlerProvider());
+
+    ExtensionPoint<BuildSystemProvider> buildSystemProviderExtensionPoint =
+        registerExtensionPoint(BuildSystemProvider.EP_NAME, BuildSystemProvider.class);
+    buildSystemProviderExtensionPoint.registerExtension(new MockBuildSystemProvider());
+
+    ExtensionPoint<DeployTargetProvider> deployTargetProviderExtensionPoint =
+        registerExtensionPoint(
+            ExtensionPointName.create("com.android.run.deployTargetProvider"),
+            DeployTargetProvider.class);
+    deployTargetProviderExtensionPoint.registerExtension(new ShowChooserTargetProvider());
+
+    ExtensionPoint<AndroidDebugger> androidDebuggerExtensionPoint =
+        registerExtensionPoint(AndroidDebugger.EP_NAME, AndroidDebugger.class);
+    androidDebuggerExtensionPoint.registerExtension(new AndroidJavaDebugger());
+
+    ExtensionPoint<BeforeRunTaskProvider> beforeRunTaskProviderExtensionPoint =
+        registerExtensionPoint(
+            ExtensionPointName.create("com.intellij.stepsBeforeRunProvider"),
+            BeforeRunTaskProvider.class);
+    ((ExtensionsAreaImpl) Extensions.getArea(project))
+        .registerExtensionPoint((ExtensionPointImpl) beforeRunTaskProviderExtensionPoint);
+    beforeRunTaskProviderExtensionPoint.registerExtension(new BlazeBeforeRunTaskProvider());
+
+    ExtensionPoint<TestRecorderRunConfigurationProxyProvider>
+        testRecorderRunConfigurationProxyProviderExtensionPoint =
+            registerExtensionPoint(
+                ExtensionPointName.create(
+                    "com.google.gct.testrecorder.run.testRecorderRunConfigurationProxyProvider"),
+                TestRecorderRunConfigurationProxyProvider.class);
+    testRecorderRunConfigurationProxyProviderExtensionPoint.registerExtension(
+        new TestRecorderBlazeCommandRunConfigurationProxyProvider());
+
+    ((MockProject) project)
+        .addComponent(
+            RunManager.class, new RunManagerImpl(project, new ProjectPropertiesComponentImpl()));
+  }
+
+  @Test
+  public void testSuitableRunConfigurations() {
+    addConfigurations();
+
+    List<RunConfiguration> allConfigurations =
+        RunManagerEx.getInstanceEx(project).getAllConfigurationsList();
+    assertThat(allConfigurations.size()).isEqualTo(2);
+
+    List<RunConfiguration> suitableConfigurations =
+        TestRecorderAction.getSuitableRunConfigurations(project);
+    assertThat(suitableConfigurations.size()).isEqualTo(1);
+    assertThat(suitableConfigurations.get(0).getName()).isEqualTo("AndroidBinaryConfiguration");
+  }
+
+  @Test
+  public void testLaunchActivityClass() {
+    BlazeCommandRunConfiguration blazeConfiguration =
+        BlazeCommandRunConfigurationType.getInstance()
+            .getFactory()
+            .createTemplateConfiguration(project);
+    blazeConfiguration.setTarget(new Label("//label:android_binary_rule"));
+    BlazeAndroidBinaryRunConfigurationState configurationState =
+        ((BlazeAndroidBinaryRunConfigurationHandler) blazeConfiguration.getHandler()).getState();
+    configurationState.setMode(BlazeAndroidBinaryRunConfigurationState.LAUNCH_SPECIFIC_ACTIVITY);
+    configurationState.setActivityClass("MyAppMainActivity");
+
+    TestRecorderRunConfigurationProxy proxy =
+        TestRecorderRunConfigurationProxy.getInstance(blazeConfiguration);
+    assertThat(proxy).isNotNull();
+    assertThat(proxy.getLaunchActivityClass()).isEqualTo("MyAppMainActivity");
+  }
+
+  private void mockBlazeImportSettings(Container projectServices) {
+    BlazeImportSettingsManager importSettingsManager = new BlazeImportSettingsManager(project);
+    importSettingsManager.setImportSettings(
+        new BlazeImportSettings("", "", "", "", "", Blaze.BuildSystem.Blaze));
+    projectServices.register(BlazeImportSettingsManager.class, importSettingsManager);
+  }
+
+  private void addConfigurations() {
+    RunManagerImpl runManager = (RunManagerImpl) RunManagerEx.getInstanceEx(project);
+    BlazeCommandRunConfigurationType.BlazeCommandRunConfigurationFactory configurationFactory =
+        BlazeCommandRunConfigurationType.getInstance().getFactory();
+
+    BlazeCommandRunConfiguration blazeAndroidBinaryConfiguration =
+        configurationFactory.createTemplateConfiguration(project);
+    blazeAndroidBinaryConfiguration.setName("AndroidBinaryConfiguration");
+    blazeAndroidBinaryConfiguration.setTarget(new Label("//label:android_binary_rule"));
+
+    BlazeCommandRunConfiguration blazeAndroidTestConfiguration =
+        configurationFactory.createTemplateConfiguration(project);
+    blazeAndroidTestConfiguration.setName("AndroidTestConfiguration");
+    blazeAndroidTestConfiguration.setTarget(new Label("//label:android_test_rule"));
+
+    runManager.addConfiguration(
+        runManager.createConfiguration(blazeAndroidBinaryConfiguration, configurationFactory),
+        true);
+    runManager.addConfiguration(
+        runManager.createConfiguration(blazeAndroidTestConfiguration, configurationFactory), true);
+  }
+
+  private class MockTargetFinder extends TargetFinder {
+    @Override
+    public List<TargetIdeInfo> findTargets(Project project, Predicate<TargetIdeInfo> predicate) {
+      return null;
+    }
+
+    @Override
+    public TargetIdeInfo targetForLabel(Project project, final Label label) {
+      TargetIdeInfo.Builder builder = TargetIdeInfo.builder().setLabel(label);
+      if (label.equals(new Label("//label:android_binary_rule"))) {
+        builder.setKind(Kind.ANDROID_BINARY);
+      } else if (label.equals(new Label("//label:android_test_rule"))) {
+        builder.setKind(Kind.ANDROID_TEST);
+      }
+      return builder.build();
+    }
+  }
+
+  private class MockBlazeAndroidBinaryRunConfigurationHandlerProvider
+      extends BlazeAndroidBinaryRunConfigurationHandlerProvider {
+    @Override
+    public boolean canHandleKind(Kind kind) {
+      return true;
+    }
+
+    @Override
+    public BlazeCommandRunConfigurationHandler createHandler(BlazeCommandRunConfiguration config) {
+      return new MockBlazeAndroidBinaryRunConfigurationHandler(config);
+    }
+  }
+
+  private class MockBlazeAndroidBinaryRunConfigurationHandler
+      extends BlazeAndroidBinaryRunConfigurationHandler {
+    private final MockModule mockModule;
+
+    MockBlazeAndroidBinaryRunConfigurationHandler(BlazeCommandRunConfiguration configuration) {
+      super(configuration);
+      mockModule = new MockModule(project, () -> {});
+    }
+
+    @Nullable
+    @Override
+    public Module getModule() {
+      Label label = getLabel();
+      if (label != null && label.equals(new Label("//label:android_binary_rule"))) {
+        return mockModule;
+      }
+
+      return null;
+    }
+  }
+
+  private class MockBuildSystemProvider implements BuildSystemProvider {
+    @Override
+    public Blaze.BuildSystem buildSystem() {
+      return Blaze.BuildSystem.Blaze;
+    }
+
+    @Override
+    public WorkspaceRootProvider getWorkspaceRootProvider() {
+      return null;
+    }
+
+    @Override
+    public ImmutableList<String> buildArtifactDirectories(WorkspaceRoot root) {
+      return null;
+    }
+
+    @Nullable
+    @Override
+    public String getRuleDocumentationUrl(RuleDefinition rule) {
+      return null;
+    }
+
+    @Override
+    public boolean isBuildFile(String fileName) {
+      return false;
+    }
+
+    @Nullable
+    @Override
+    public File findBuildFileInDirectory(File directory) {
+      return null;
+    }
+
+    @Override
+    public FileNameMatcher buildFileMatcher() {
+      return null;
+    }
+
+    @Override
+    public void populateBlazeVersionData(
+        BuildSystem buildSystem,
+        WorkspaceRoot workspaceRoot,
+        ImmutableMap<String, String> blazeInfo,
+        BlazeVersionData.Builder builder) {}
+  }
+}
diff --git a/aswb/src/com/google/idea/blaze/android/run/binary/instantrun/InstantRunExperiment.java b/aswb/2.3/tests/utils/integration/com/google/idea/blaze/android/BlazeAndroidIntegrationTestCase.java
similarity index 65%
rename from aswb/src/com/google/idea/blaze/android/run/binary/instantrun/InstantRunExperiment.java
rename to aswb/2.3/tests/utils/integration/com/google/idea/blaze/android/BlazeAndroidIntegrationTestCase.java
index 148b1d9..74459a0 100644
--- a/aswb/src/com/google/idea/blaze/android/run/binary/instantrun/InstantRunExperiment.java
+++ b/aswb/2.3/tests/utils/integration/com/google/idea/blaze/android/BlazeAndroidIntegrationTestCase.java
@@ -13,12 +13,10 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package com.google.idea.blaze.android.run.binary.instantrun;
 
-import com.google.idea.common.experiments.BoolExperiment;
+package com.google.idea.blaze.android;
 
-/** Holds the instant run experiment */
-public class InstantRunExperiment {
-  public static final BoolExperiment INSTANT_RUN_ENABLED =
-      new BoolExperiment("instant.run.enabled", false);
-}
+import com.google.idea.blaze.base.BlazeIntegrationTestCase;
+
+/** Compatibility test class for Blaze Android integration tests. */
+public abstract class BlazeAndroidIntegrationTestCase extends BlazeIntegrationTestCase {}
diff --git a/aswb/BUILD b/aswb/BUILD
index f3cfd00..16fc99b 100644
--- a/aswb/BUILD
+++ b/aswb/BUILD
@@ -11,6 +11,7 @@
     "stamped_plugin_xml",
 )
 load("//:version.bzl", "VERSION")
+load("//intellij_platform_sdk:build_defs.bzl", "select_for_plugin_api")
 
 merged_plugin_xml(
     name = "merged_plugin_xml_common",
@@ -19,7 +20,12 @@
         "//base:plugin_xml",
         "//cpp:plugin_xml",
         "//java:plugin_xml",
-    ],
+    ] + select_for_plugin_api({
+        # TODO(chaorenl): remove when 2.2 is obsolete
+        "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"],
+    }),
     visibility = [
         "//visibility:public",
     ],
@@ -45,7 +51,12 @@
 
 java_library(
     name = "aswb_lib",
-    srcs = glob(["src/**/*.java"]),
+    srcs = glob(["src/**/*.java"]) + select_for_plugin_api({
+        # TODO(chaorenl): remove when 2.2 is obsolete
+        "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"]),
+    }),
     resources = glob(["resources/**/*"]),
     visibility = [
         "//visibility:public",
@@ -61,10 +72,15 @@
     ],
 )
 
+# TODO(chaorenl): remove when 2.2 is obsolete
 java_library(
     name = "integration_test_utils",
     testonly = 1,
-    srcs = glob(["tests/utils/integration/**/*.java"]),
+    srcs = select_for_plugin_api({
+        "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"]),
+    }),
     deps = [
         "//base",
         "//base:integration_test_utils",
@@ -83,7 +99,12 @@
 
 intellij_unit_test_suite(
     name = "unit_tests",
-    srcs = glob(["tests/unittests/**/*.java"]),
+    srcs = glob(["tests/unittests/**/*.java"]) + select_for_plugin_api({
+        # TODO(chaorenl): remove when 2.2 is obsolete
+        "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"]),
+    }),
     test_package_root = "com.google.idea.blaze.android",
     deps = [
         ":aswb_lib",
@@ -101,7 +122,12 @@
 
 intellij_integration_test_suite(
     name = "integration_tests",
-    srcs = glob(["tests/integrationtests/**/*.java"]),
+    srcs = glob(["tests/integrationtests/**/*.java"]) + select_for_plugin_api({
+        # TODO(chaorenl): remove when 2.2 is obsolete
+        "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"]),
+    }),
     platform_prefix = "AndroidStudio",
     required_plugins = "com.google.idea.bazel.aswb",
     test_package_root = "com.google.idea.blaze.android",
diff --git a/aswb/aswb.bazelproject b/aswb/aswb.bazelproject
index 34cbb52..c2f7d36 100644
--- a/aswb/aswb.bazelproject
+++ b/aswb/aswb.bazelproject
@@ -4,6 +4,8 @@
   -plugin_dev
   -clwb
   -cpp/src/com/google/idea/blaze/cpp/versioned/v162
+  # TODO(chaorenl): remove when 2.2 is obsolete.
+  -aswb/2.3
 
 targets:
   //aswb:aswb_bazel
diff --git a/aswb/src/META-INF/aswb.xml b/aswb/src/META-INF/aswb.xml
index ca22494..48f701d 100644
--- a/aswb/src/META-INF/aswb.xml
+++ b/aswb/src/META-INF/aswb.xml
@@ -18,7 +18,8 @@
 
   <depends>com.intellij.modules.androidstudio</depends>
   <depends>org.jetbrains.android</depends>
-  <depends>com.android.tools.idea.updater</depends>
+  <!-- TODO(chaorenl): remove when 2.2 is obsolete -->
+  <depends optional="true">com.android.tools.idea.updater</depends>
 
   <extensions defaultExtensionNs="com.intellij">
     <java.elementFinder implementation="com.google.idea.blaze.android.resources.AndroidResourceClassFinder"
@@ -31,14 +32,14 @@
     <runConfigurationProducer
         implementation="com.google.idea.blaze.android.run.test.BlazeAndroidTestMethodRunConfigurationProducer"
         order="first"/>
-    <configurationType implementation="com.google.idea.blaze.android.run.binary.BlazeAndroidBinaryRunConfigurationType"/>
-    <configurationType implementation="com.google.idea.blaze.android.run.test.BlazeAndroidTestRunConfigurationType"/>
     <programRunner implementation="com.google.idea.blaze.android.run.binary.BlazeAndroidBinaryProgramRunner" order="first"/>
     <executor implementation="com.google.idea.blaze.android.run.binary.mobileinstall.IncrementalInstallRunExecutor" order="last"/>
     <executor implementation="com.google.idea.blaze.android.run.binary.mobileinstall.IncrementalInstallDebugExecutor" order="last"/>
     <applicationService serviceInterface="com.google.idea.blaze.base.plugin.BlazePluginId"
                         serviceImplementation="com.google.idea.blaze.android.plugin.AswbPlugin"/>
     <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"/>
   </extensions>
 
   <extensions defaultExtensionNs="org.jetbrains.android.actions">
@@ -88,4 +89,4 @@
     </component>
   </application-components>
 
-</idea-plugin>
\ No newline at end of file
+</idea-plugin>
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 056f692..0c14f79 100644
--- a/aswb/src/com/google/idea/blaze/android/projectview/AndroidSdkPlatformSection.java
+++ b/aswb/src/com/google/idea/blaze/android/projectview/AndroidSdkPlatformSection.java
@@ -15,6 +15,7 @@
  */
 package com.google.idea.blaze.android.projectview;
 
+import com.google.idea.blaze.android.compatibility.Compatibility.AndroidSdkUtils;
 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;
@@ -28,7 +29,6 @@
 import com.intellij.openapi.projectRoots.Sdk;
 import com.intellij.openapi.util.text.StringUtil;
 import java.util.Collection;
-import org.jetbrains.android.sdk.AndroidSdkUtils;
 import org.jetbrains.annotations.Nullable;
 
 /** Allows manual override of the android sdk. */
diff --git a/aswb/src/com/google/idea/blaze/android/run/BlazeAndroidRunConfigurationCommonState.java b/aswb/src/com/google/idea/blaze/android/run/BlazeAndroidRunConfigurationCommonState.java
index 5bd0c77..6926023 100644
--- a/aswb/src/com/google/idea/blaze/android/run/BlazeAndroidRunConfigurationCommonState.java
+++ b/aswb/src/com/google/idea/blaze/android/run/BlazeAndroidRunConfigurationCommonState.java
@@ -215,5 +215,11 @@
       }
       return UiUtil.createBox(result);
     }
+
+    @Override
+    public void setComponentEnabled(boolean enabled) {
+      userFlagsEditor.setComponentEnabled(enabled);
+      enableNativeDebuggingCheckBox.setEnabled(enabled);
+    }
   }
 }
diff --git a/aswb/src/com/google/idea/blaze/android/run/binary/BlazeAndroidBinaryNormalBuildRunContext.java b/aswb/src/com/google/idea/blaze/android/run/binary/BlazeAndroidBinaryNormalBuildRunContext.java
index b90cc35..5351213 100644
--- a/aswb/src/com/google/idea/blaze/android/run/binary/BlazeAndroidBinaryNormalBuildRunContext.java
+++ b/aswb/src/com/google/idea/blaze/android/run/binary/BlazeAndroidBinaryNormalBuildRunContext.java
@@ -33,6 +33,7 @@
 import com.android.tools.idea.run.util.ProcessHandlerLaunchStatus;
 import com.google.common.collect.ImmutableList;
 import com.google.common.util.concurrent.Futures;
+import com.google.idea.blaze.android.compatibility.Compatibility;
 import com.google.idea.blaze.android.run.deployinfo.BlazeAndroidDeployInfo;
 import com.google.idea.blaze.android.run.deployinfo.BlazeApkProvider;
 import com.google.idea.blaze.android.run.runner.BlazeAndroidDeviceSelector;
@@ -167,14 +168,22 @@
 
   @Nullable
   @Override
+  @SuppressWarnings("unchecked")
   public DebugConnectorTask getDebuggerTask(
       AndroidDebugger androidDebugger,
       AndroidDebuggerState androidDebuggerState,
-      Set<String> packageIds)
+      Set<String> packageIds,
+      boolean monitorRemoteProcess)
       throws ExecutionException {
-    //noinspection unchecked
-    return androidDebugger.getConnectDebuggerTask(
-        env, null, packageIds, facet, androidDebuggerState, runConfiguration.getType().getId());
+    return Compatibility.getConnectDebuggerTask(
+        androidDebugger,
+        env,
+        null,
+        packageIds,
+        facet,
+        androidDebuggerState,
+        runConfiguration.getType().getId(),
+        monitorRemoteProcess);
   }
 
   @Nullable
diff --git a/aswb/src/com/google/idea/blaze/android/run/binary/BlazeAndroidBinaryRunConfigurationHandler.java b/aswb/src/com/google/idea/blaze/android/run/binary/BlazeAndroidBinaryRunConfigurationHandler.java
index a70da60..3b222f7 100644
--- a/aswb/src/com/google/idea/blaze/android/run/binary/BlazeAndroidBinaryRunConfigurationHandler.java
+++ b/aswb/src/com/google/idea/blaze/android/run/binary/BlazeAndroidBinaryRunConfigurationHandler.java
@@ -15,16 +15,13 @@
  */
 package com.google.idea.blaze.android.run.binary;
 
-import com.android.sdklib.AndroidVersion;
-import com.android.tools.idea.fd.InstantRunManager;
-import com.android.tools.idea.run.AndroidSessionInfo;
+import com.android.annotations.VisibleForTesting;
 import com.android.tools.idea.run.ValidationError;
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.Lists;
 import com.google.idea.blaze.android.run.BlazeAndroidRunConfigurationCommonState;
 import com.google.idea.blaze.android.run.BlazeAndroidRunConfigurationHandler;
 import com.google.idea.blaze.android.run.BlazeAndroidRunConfigurationValidationUtil;
-import com.google.idea.blaze.android.run.binary.instantrun.BlazeAndroidBinaryInstantRunContext;
 import com.google.idea.blaze.android.run.binary.mobileinstall.BlazeAndroidBinaryMobileInstallRunContext;
 import com.google.idea.blaze.android.run.runner.BlazeAndroidRunConfigurationRunner;
 import com.google.idea.blaze.android.run.runner.BlazeAndroidRunContext;
@@ -42,15 +39,12 @@
 import com.intellij.execution.Executor;
 import com.intellij.execution.configurations.RunConfiguration;
 import com.intellij.execution.configurations.RuntimeConfigurationException;
-import com.intellij.execution.executors.DefaultRunExecutor;
 import com.intellij.execution.runners.ExecutionEnvironment;
 import com.intellij.openapi.module.Module;
 import com.intellij.openapi.project.Project;
-import icons.AndroidIcons;
 import java.util.List;
 import javax.swing.Icon;
 import org.jetbrains.android.facet.AndroidFacet;
-import org.jetbrains.annotations.NotNull;
 import org.jetbrains.annotations.Nullable;
 
 /**
@@ -63,7 +57,8 @@
   private final BlazeCommandRunConfiguration configuration;
   private final BlazeAndroidBinaryRunConfigurationState configState;
 
-  BlazeAndroidBinaryRunConfigurationHandler(BlazeCommandRunConfiguration configuration) {
+  @VisibleForTesting
+  protected BlazeAndroidBinaryRunConfigurationHandler(BlazeCommandRunConfiguration configuration) {
     this.configuration = configuration;
     configState =
         new BlazeAndroidBinaryRunConfigurationState(
@@ -91,7 +86,7 @@
   }
 
   @Nullable
-  private Module getModule() {
+  public Module getModule() {
     Label target = getLabel();
     return target != null
         ? BlazeAndroidProjectStructureSyncer.ensureRunConfigurationModule(
@@ -125,10 +120,7 @@
       AndroidFacet facet,
       ExecutionEnvironment env,
       ImmutableList<String> buildFlags) {
-    if (configState.instantRun()) {
-      return new BlazeAndroidBinaryInstantRunContext(
-          project, facet, configuration, env, configState, getLabel(), buildFlags);
-    } else if (configState.mobileInstall()) {
+    if (configState.mobileInstall()) {
       return new BlazeAndroidBinaryMobileInstallRunContext(
           project, facet, configuration, env, configState, getLabel(), buildFlags);
     } else {
@@ -187,27 +179,7 @@
 
   @Override
   @Nullable
-  public Icon getExecutorIcon(@NotNull RunConfiguration configuration, @NotNull Executor executor) {
-    if (!configState.instantRun()) {
-      return null;
-    }
-
-    AndroidSessionInfo info =
-        AndroidSessionInfo.findOldSession(
-            this.configuration.getProject(), null, this.configuration.getUniqueID());
-    if (info == null || !info.isInstantRun() || !info.getExecutorId().equals(executor.getId())) {
-      return null;
-    }
-
-    // Make sure instant run is supported on the relevant device, if found.
-    AndroidVersion androidVersion =
-        InstantRunManager.getMinDeviceApiLevel(info.getProcessHandler());
-    if (!InstantRunManager.isInstantRunCapableDeviceVersion(androidVersion)) {
-      return null;
-    }
-
-    return executor instanceof DefaultRunExecutor
-        ? AndroidIcons.RunIcons.Replay
-        : AndroidIcons.RunIcons.DebugReattach;
+  public Icon getExecutorIcon(RunConfiguration configuration, Executor executor) {
+    return null;
   }
 }
diff --git a/aswb/src/com/google/idea/blaze/android/run/binary/BlazeAndroidBinaryRunConfigurationState.java b/aswb/src/com/google/idea/blaze/android/run/binary/BlazeAndroidBinaryRunConfigurationState.java
index 6557f8b..cb368d4 100644
--- a/aswb/src/com/google/idea/blaze/android/run/binary/BlazeAndroidBinaryRunConfigurationState.java
+++ b/aswb/src/com/google/idea/blaze/android/run/binary/BlazeAndroidBinaryRunConfigurationState.java
@@ -42,12 +42,10 @@
 
   private static final String MOBILE_INSTALL_ATTR = "blaze-mobile-install";
   private static final String USE_SPLIT_APKS_IF_POSSIBLE = "use-split-apks-if-possible";
-  private static final String INSTANT_RUN_ATTR = "instant-run";
   private static final String WORK_PROFILE_ATTR = "use-work-profile-if-present";
   private static final String USER_ID_ATTR = "user-id";
   private boolean mobileInstall = false;
   private boolean useSplitApksIfPossible = false;
-  private boolean instantRun = false;
   private boolean useWorkProfileIfPresent = false;
   private Integer userId;
 
@@ -85,14 +83,6 @@
     this.useSplitApksIfPossible = useSplitApksIfPossible;
   }
 
-  boolean instantRun() {
-    return instantRun;
-  }
-
-  void setInstantRun(boolean instantRun) {
-    this.instantRun = instantRun;
-  }
-
   public boolean useWorkProfileIfPresent() {
     return useWorkProfileIfPresent;
   }
@@ -156,7 +146,6 @@
     setMobileInstall(Boolean.parseBoolean(element.getAttributeValue(MOBILE_INSTALL_ATTR)));
     setUseSplitApksIfPossible(
         Boolean.parseBoolean(element.getAttributeValue(USE_SPLIT_APKS_IF_POSSIBLE)));
-    setInstantRun(Boolean.parseBoolean(element.getAttributeValue(INSTANT_RUN_ATTR)));
     setUseWorkProfileIfPresent(Boolean.parseBoolean(element.getAttributeValue(WORK_PROFILE_ATTR)));
 
     String userIdString = element.getAttributeValue(USER_ID_ATTR);
@@ -196,7 +185,6 @@
     element.setAttribute(MODE, mode);
     element.setAttribute(MOBILE_INSTALL_ATTR, Boolean.toString(mobileInstall));
     element.setAttribute(USE_SPLIT_APKS_IF_POSSIBLE, Boolean.toString(useSplitApksIfPossible));
-    element.setAttribute(INSTANT_RUN_ATTR, Boolean.toString(instantRun));
     element.setAttribute(WORK_PROFILE_ATTR, Boolean.toString(useWorkProfileIfPresent));
 
     if (userId != null) {
diff --git a/aswb/src/com/google/idea/blaze/android/run/binary/BlazeAndroidBinaryRunConfigurationStateEditor.java b/aswb/src/com/google/idea/blaze/android/run/binary/BlazeAndroidBinaryRunConfigurationStateEditor.java
index c7fbff6..8ce6279 100644
--- a/aswb/src/com/google/idea/blaze/android/run/binary/BlazeAndroidBinaryRunConfigurationStateEditor.java
+++ b/aswb/src/com/google/idea/blaze/android/run/binary/BlazeAndroidBinaryRunConfigurationStateEditor.java
@@ -16,7 +16,6 @@
 package com.google.idea.blaze.android.run.binary;
 
 import com.android.tools.idea.run.activity.ActivityLocatorUtils;
-import com.google.idea.blaze.android.run.binary.instantrun.InstantRunExperiment;
 import com.google.idea.blaze.base.run.state.RunConfigurationState;
 import com.google.idea.blaze.base.run.state.RunConfigurationStateEditor;
 import com.google.idea.blaze.base.ui.IntegerTextField;
@@ -75,11 +74,12 @@
   private JRadioButton launchCustomButton;
   private JCheckBox mobileInstallCheckBox;
   private JCheckBox splitApksCheckBox;
-  private JCheckBox instantRunCheckBox;
   private JCheckBox useWorkProfileIfPresentCheckBox;
   private JLabel userIdLabel;
   private IntegerTextField userIdField;
 
+  private boolean componentEnabled = true;
+
   BlazeAndroidBinaryRunConfigurationStateEditor(
       RunConfigurationStateEditor commonStateEditor, Project project) {
     this.commonStateEditor = commonStateEditor;
@@ -126,34 +126,15 @@
             }
           }
         });
-    ActionListener listener = e -> activityField.setEnabled(launchCustomButton.isSelected());
+    ActionListener listener = e -> updateEnabledState();
     launchCustomButton.addActionListener(listener);
     launchDefaultButton.addActionListener(listener);
     launchNothingButton.addActionListener(listener);
 
-    instantRunCheckBox.setVisible(InstantRunExperiment.INSTANT_RUN_ENABLED.getValue());
-
-    /* Only one of mobile-install and instant run can be selected at any one time */
-    mobileInstallCheckBox.addActionListener(
-        e -> {
-          if (mobileInstallCheckBox.isSelected()) {
-            instantRunCheckBox.setSelected(false);
-          }
-        });
-    instantRunCheckBox.addActionListener(
-        e -> {
-          if (instantRunCheckBox.isSelected()) {
-            mobileInstallCheckBox.setSelected(false);
-          }
-        });
-
     mobileInstallCheckBox.addActionListener(
         e -> splitApksCheckBox.setVisible(mobileInstallCheckBox.isSelected()));
 
-    useWorkProfileIfPresentCheckBox.addActionListener(
-        e -> {
-          setUserIdEnabled(!useWorkProfileIfPresentCheckBox.isSelected());
-        });
+    useWorkProfileIfPresentCheckBox.addActionListener(e -> updateEnabledState());
   }
 
   @Override
@@ -170,24 +151,18 @@
     } else {
       launchNothingButton.setSelected(true);
     }
-    activityField.setEnabled(launchSpecificActivity);
     if (launchSpecificActivity) {
       activityField.getChildComponent().setText(state.getActivityClass());
     }
 
     mobileInstallCheckBox.setSelected(state.mobileInstall());
     splitApksCheckBox.setSelected(state.useSplitApksIfPossible());
-    instantRunCheckBox.setSelected(state.instantRun());
     useWorkProfileIfPresentCheckBox.setSelected(state.useWorkProfileIfPresent());
 
     userIdField.setValue(state.getUserId());
-    setUserIdEnabled(!state.useWorkProfileIfPresent());
     splitApksCheckBox.setVisible(state.mobileInstall());
-  }
 
-  private void setUserIdEnabled(boolean enabled) {
-    userIdLabel.setEnabled(enabled);
-    userIdField.setEnabled(enabled);
+    updateEnabledState();
   }
 
   @Override
@@ -207,7 +182,6 @@
     }
     state.setMobileInstall(mobileInstallCheckBox.isSelected());
     state.setUseSplitApksIfPossible(splitApksCheckBox.isSelected());
-    state.setInstantRun(instantRunCheckBox.isSelected());
     state.setUseWorkProfileIfPresent(useWorkProfileIfPresentCheckBox.isSelected());
   }
 
@@ -216,6 +190,26 @@
     return UiUtil.createBox(commonStateEditor.createComponent(), panel);
   }
 
+  private void updateEnabledState() {
+    boolean useWorkProfile = useWorkProfileIfPresentCheckBox.isSelected();
+    userIdLabel.setEnabled(componentEnabled && !useWorkProfile);
+    userIdField.setEnabled(componentEnabled && !useWorkProfile);
+    commonStateEditor.setComponentEnabled(componentEnabled);
+    activityField.setEnabled(componentEnabled && launchCustomButton.isSelected());
+    launchNothingButton.setEnabled(componentEnabled);
+    launchDefaultButton.setEnabled(componentEnabled);
+    launchCustomButton.setEnabled(componentEnabled);
+    mobileInstallCheckBox.setEnabled(componentEnabled);
+    splitApksCheckBox.setEnabled(componentEnabled);
+    useWorkProfileIfPresentCheckBox.setEnabled(componentEnabled);
+  }
+
+  @Override
+  public void setComponentEnabled(boolean enabled) {
+    componentEnabled = enabled;
+    updateEnabledState();
+  }
+
   private void createUIComponents(Project project) {
     final EditorTextField editorTextField =
         new LanguageTextField(PlainTextLanguage.INSTANCE, project, "") {
@@ -465,24 +459,6 @@
             null,
             0,
             false));
-    instantRunCheckBox = new JCheckBox();
-    instantRunCheckBox.setText(" Use InstantRun");
-    panel.add(
-        instantRunCheckBox,
-        new GridConstraints(
-            2,
-            0,
-            1,
-            2,
-            GridConstraints.ANCHOR_WEST,
-            GridConstraints.FILL_NONE,
-            GridConstraints.SIZEPOLICY_CAN_SHRINK | GridConstraints.SIZEPOLICY_CAN_GROW,
-            GridConstraints.SIZEPOLICY_FIXED,
-            null,
-            null,
-            null,
-            0,
-            false));
     ButtonGroup buttonGroup;
     buttonGroup = new ButtonGroup();
     buttonGroup.add(launchDefaultButton);
diff --git a/aswb/src/com/google/idea/blaze/android/run/binary/BlazeAndroidBinaryRunConfigurationType.java b/aswb/src/com/google/idea/blaze/android/run/binary/BlazeAndroidBinaryRunConfigurationType.java
deleted file mode 100644
index bfc5a02..0000000
--- a/aswb/src/com/google/idea/blaze/android/run/binary/BlazeAndroidBinaryRunConfigurationType.java
+++ /dev/null
@@ -1,125 +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.run.binary;
-
-import com.google.idea.blaze.base.run.BlazeCommandRunConfiguration;
-import com.google.idea.blaze.base.run.BlazeCommandRunConfigurationType;
-import com.google.idea.blaze.base.settings.Blaze;
-import com.intellij.execution.BeforeRunTask;
-import com.intellij.execution.configurations.ConfigurationFactory;
-import com.intellij.execution.configurations.ConfigurationType;
-import com.intellij.execution.configurations.ConfigurationTypeUtil;
-import com.intellij.execution.configurations.UnknownConfigurationType;
-import com.intellij.openapi.project.Project;
-import com.intellij.openapi.util.Key;
-import icons.AndroidIcons;
-import javax.swing.Icon;
-import org.jetbrains.annotations.NotNull;
-
-/**
- * A type for Android application run configurations adapted specifically to run android_binary
- * targets.
- *
- * @deprecated See {@link com.google.idea.blaze.base.run.BlazeCommandRunConfigurationType}. Retained
- *     in 1.9 for legacy purposes, to allow existing BlazeAndroidBinaryRunConfigurations to be
- *     updated to BlazeCommandRunConfigurations. Intended to be removed in 2.1.
- */
-// Hack: extend UnknownConfigurationType to completely hide it in the Run/Debug Configurations UI.
-@Deprecated
-public class BlazeAndroidBinaryRunConfigurationType extends UnknownConfigurationType {
-  private final BlazeAndroidBinaryRunConfigurationFactory factory =
-      new BlazeAndroidBinaryRunConfigurationFactory(this);
-
-  static class BlazeAndroidBinaryRunConfigurationFactory extends ConfigurationFactory {
-
-    protected BlazeAndroidBinaryRunConfigurationFactory(@NotNull ConfigurationType type) {
-      super(type);
-    }
-
-    @Override
-    public String getName() {
-      // Used to look up this ConfigurationFactory.
-      // Preserve value so legacy configurations can be loaded.
-      return Blaze.defaultBuildSystemName() + " Android Binary";
-    }
-
-    @Override
-    @NotNull
-    public BlazeCommandRunConfiguration createTemplateConfiguration(@NotNull Project project) {
-      // Create a BlazeCommandRunConfiguration instead, to update legacy configurations.
-      return BlazeCommandRunConfigurationType.getInstance()
-          .getFactory()
-          .createTemplateConfiguration(project);
-    }
-
-    @Override
-    public boolean canConfigurationBeSingleton() {
-      return false;
-    }
-
-    @Override
-    public boolean isApplicable(@NotNull Project project) {
-      return false;
-    }
-
-    @Override
-    public void configureBeforeRunTaskDefaults(
-        Key<? extends BeforeRunTask> providerID, BeforeRunTask task) {
-      // Removed BlazeAndroidBeforeRunTaskProvider; this method won't be called anymore anyhow.
-    }
-
-    @Override
-    public boolean isConfigurationSingletonByDefault() {
-      return false;
-    }
-  }
-
-  @NotNull
-  public static BlazeAndroidBinaryRunConfigurationType getInstance() {
-    return ConfigurationTypeUtil.findConfigurationType(
-        BlazeAndroidBinaryRunConfigurationType.class);
-  }
-
-  @Override
-  @NotNull
-  public String getDisplayName() {
-    return "Legacy " + Blaze.defaultBuildSystemName() + " Android Binary";
-  }
-
-  @Override
-  public String getConfigurationTypeDescription() {
-    return "Launch/debug configuration for android_binary rules. "
-        + "Use Blaze Command instead; this legacy configuration type is being removed.";
-  }
-
-  @Override
-  public Icon getIcon() {
-    return AndroidIcons.Android;
-  }
-
-  @Override
-  @NotNull
-  public String getId() {
-    // Used to look up this ConfigurationType.
-    // Preserve value so legacy configurations can be loaded.
-    return "BlazeAndroidBinaryRunConfigurationType";
-  }
-
-  @Override
-  public BlazeAndroidBinaryRunConfigurationFactory[] getConfigurationFactories() {
-    return new BlazeAndroidBinaryRunConfigurationFactory[] {factory};
-  }
-}
diff --git a/aswb/src/com/google/idea/blaze/android/run/binary/BlazeDefaultActivityLocator.java b/aswb/src/com/google/idea/blaze/android/run/binary/BlazeDefaultActivityLocator.java
index d6fcd4a..50c2dfd 100644
--- a/aswb/src/com/google/idea/blaze/android/run/binary/BlazeDefaultActivityLocator.java
+++ b/aswb/src/com/google/idea/blaze/android/run/binary/BlazeDefaultActivityLocator.java
@@ -45,7 +45,13 @@
   @NotNull
   @Override
   public String getQualifiedActivityName(@NotNull IDevice device) throws ActivityLocatorException {
-    Manifest manifest = ManifestParser.getInstance(project).getManifest(mergedManifestFile);
+    // Run in a read action since otherwise, it might throw a read access exception.
+    Manifest manifest =
+        ApplicationManager.getApplication()
+            .runReadAction(
+                (Computable<Manifest>)
+                    () -> ManifestParser.getInstance(project).getManifest(mergedManifestFile));
+
     if (manifest == null) {
       throw new ActivityLocatorException("Could not locate merged manifest");
     }
diff --git a/aswb/src/com/google/idea/blaze/android/run/binary/instantrun/BlazeAndroidBinaryInstantRunContext.java b/aswb/src/com/google/idea/blaze/android/run/binary/instantrun/BlazeAndroidBinaryInstantRunContext.java
deleted file mode 100644
index b35d0b2..0000000
--- a/aswb/src/com/google/idea/blaze/android/run/binary/instantrun/BlazeAndroidBinaryInstantRunContext.java
+++ /dev/null
@@ -1,196 +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.run.binary.instantrun;
-
-import com.android.ddmlib.IDevice;
-import com.android.tools.idea.fd.InstantRunBuildAnalyzer;
-import com.android.tools.idea.fd.InstantRunUtils;
-import com.android.tools.idea.run.ApplicationIdProvider;
-import com.android.tools.idea.run.ConsolePrinter;
-import com.android.tools.idea.run.ConsoleProvider;
-import com.android.tools.idea.run.LaunchOptions;
-import com.android.tools.idea.run.activity.DefaultStartActivityFlagsProvider;
-import com.android.tools.idea.run.activity.StartActivityFlagsProvider;
-import com.android.tools.idea.run.editor.AndroidDebugger;
-import com.android.tools.idea.run.editor.AndroidDebuggerState;
-import com.android.tools.idea.run.tasks.DebugConnectorTask;
-import com.android.tools.idea.run.tasks.LaunchTask;
-import com.android.tools.idea.run.tasks.LaunchTasksProvider;
-import com.android.tools.idea.run.tasks.UpdateSessionTasksProvider;
-import com.android.tools.idea.run.util.ProcessHandlerLaunchStatus;
-import com.google.common.collect.ImmutableList;
-import com.google.common.util.concurrent.Futures;
-import com.google.idea.blaze.android.run.binary.BlazeAndroidBinaryApplicationLaunchTaskProvider;
-import com.google.idea.blaze.android.run.binary.BlazeAndroidBinaryConsoleProvider;
-import com.google.idea.blaze.android.run.binary.BlazeAndroidBinaryRunConfigurationState;
-import com.google.idea.blaze.android.run.binary.UserIdHelper;
-import com.google.idea.blaze.android.run.runner.BlazeAndroidDeviceSelector;
-import com.google.idea.blaze.android.run.runner.BlazeAndroidLaunchTasksProvider;
-import com.google.idea.blaze.android.run.runner.BlazeAndroidRunConfigurationDebuggerManager;
-import com.google.idea.blaze.android.run.runner.BlazeAndroidRunContext;
-import com.google.idea.blaze.android.run.runner.BlazeApkBuildStep;
-import com.google.idea.blaze.base.model.primitives.Label;
-import com.intellij.execution.ExecutionException;
-import com.intellij.execution.configurations.RunConfiguration;
-import com.intellij.execution.runners.ExecutionEnvironment;
-import com.intellij.openapi.project.Project;
-import java.util.Set;
-import javax.annotation.Nullable;
-import org.jetbrains.android.facet.AndroidFacet;
-import org.jetbrains.annotations.NotNull;
-
-/** Run context for InstantRun. */
-public class BlazeAndroidBinaryInstantRunContext implements BlazeAndroidRunContext {
-
-  private final Project project;
-  private final AndroidFacet facet;
-  private final RunConfiguration runConfiguration;
-  private final ExecutionEnvironment env;
-  private final BlazeAndroidBinaryRunConfigurationState configState;
-
-  private final BlazeAndroidBinaryConsoleProvider consoleProvider;
-  private final BlazeApkBuildStepInstantRun buildStep;
-
-  public BlazeAndroidBinaryInstantRunContext(
-      Project project,
-      AndroidFacet facet,
-      RunConfiguration runConfiguration,
-      ExecutionEnvironment env,
-      BlazeAndroidBinaryRunConfigurationState configState,
-      Label label,
-      ImmutableList<String> buildFlags) {
-    this.project = project;
-    this.facet = facet;
-    this.runConfiguration = runConfiguration;
-    this.env = env;
-    this.configState = configState;
-    this.consoleProvider = new BlazeAndroidBinaryConsoleProvider(project);
-    this.buildStep = new BlazeApkBuildStepInstantRun(project, env, label, buildFlags);
-  }
-
-  @Override
-  public BlazeAndroidDeviceSelector getDeviceSelector() {
-    return new BlazeInstantRunDeviceSelector();
-  }
-
-  @Override
-  public void augmentEnvironment(ExecutionEnvironment env) {
-    InstantRunUtils.setInstantRunEnabled(env, true);
-  }
-
-  @Override
-  public void augmentLaunchOptions(@NotNull LaunchOptions.Builder options) {
-    options.setDeploy(true).setOpenLogcatAutomatically(true);
-  }
-
-  @NotNull
-  @Override
-  public ConsoleProvider getConsoleProvider() {
-    return consoleProvider;
-  }
-
-  @Override
-  public ApplicationIdProvider getApplicationIdProvider() throws ExecutionException {
-    return Futures.get(buildStep.getApplicationIdProvider(), ExecutionException.class);
-  }
-
-  @Override
-  public BlazeApkBuildStep getBuildStep() {
-    return buildStep;
-  }
-
-  @Override
-  public LaunchTasksProvider getLaunchTasksProvider(
-      LaunchOptions.Builder launchOptionsBuilder,
-      boolean isDebug,
-      BlazeAndroidRunConfigurationDebuggerManager debuggerManager)
-      throws ExecutionException {
-    InstantRunBuildAnalyzer analyzer =
-        Futures.get(buildStep.getInstantRunBuildAnalyzer(), ExecutionException.class);
-
-    if (analyzer.canReuseProcessHandler()) {
-      return new UpdateSessionTasksProvider(analyzer);
-    }
-    return new BlazeAndroidLaunchTasksProvider(
-        project,
-        this,
-        getApplicationIdProvider(),
-        launchOptionsBuilder,
-        isDebug,
-        true,
-        debuggerManager);
-  }
-
-  @Override
-  public ImmutableList<LaunchTask> getDeployTasks(IDevice device, LaunchOptions launchOptions)
-      throws ExecutionException {
-    InstantRunBuildAnalyzer analyzer =
-        Futures.get(buildStep.getInstantRunBuildAnalyzer(), ExecutionException.class);
-    return ImmutableList.<LaunchTask>builder()
-        .addAll(analyzer.getDeployTasks(launchOptions))
-        .add(analyzer.getNotificationTask())
-        .build();
-  }
-
-  @Nullable
-  @Override
-  public LaunchTask getApplicationLaunchTask(
-      LaunchOptions launchOptions,
-      @Nullable Integer userId,
-      AndroidDebugger androidDebugger,
-      AndroidDebuggerState androidDebuggerState,
-      ProcessHandlerLaunchStatus processHandlerLaunchStatus)
-      throws ExecutionException {
-    BlazeApkBuildStepInstantRun.BuildResult buildResult =
-        Futures.get(buildStep.getBuildResult(), ExecutionException.class);
-
-    final StartActivityFlagsProvider startActivityFlagsProvider =
-        new DefaultStartActivityFlagsProvider(
-            androidDebugger,
-            androidDebuggerState,
-            project,
-            launchOptions.isDebug(),
-            UserIdHelper.getFlagsFromUserId(userId));
-
-    ApplicationIdProvider applicationIdProvider = getApplicationIdProvider();
-    return BlazeAndroidBinaryApplicationLaunchTaskProvider.getApplicationLaunchTask(
-        project,
-        applicationIdProvider,
-        buildResult.mergedManifestFile,
-        configState,
-        startActivityFlagsProvider,
-        processHandlerLaunchStatus);
-  }
-
-  @Nullable
-  @Override
-  public DebugConnectorTask getDebuggerTask(
-      AndroidDebugger androidDebugger,
-      AndroidDebuggerState androidDebuggerState,
-      Set<String> packageIds)
-      throws ExecutionException {
-    //noinspection unchecked
-    return androidDebugger.getConnectDebuggerTask(
-        env, null, packageIds, facet, androidDebuggerState, runConfiguration.getType().getId());
-  }
-
-  @Nullable
-  @Override
-  public Integer getUserId(IDevice device, ConsolePrinter consolePrinter)
-      throws ExecutionException {
-    return UserIdHelper.getUserIdFromConfigurationState(device, consolePrinter, configState);
-  }
-}
diff --git a/aswb/src/com/google/idea/blaze/android/run/binary/instantrun/BlazeApkBuildStepInstantRun.java b/aswb/src/com/google/idea/blaze/android/run/binary/instantrun/BlazeApkBuildStepInstantRun.java
deleted file mode 100644
index 2830533..0000000
--- a/aswb/src/com/google/idea/blaze/android/run/binary/instantrun/BlazeApkBuildStepInstantRun.java
+++ /dev/null
@@ -1,384 +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.run.binary.instantrun;
-
-import com.android.ddmlib.IDevice;
-import com.android.tools.idea.fd.InstantRunBuildAnalyzer;
-import com.android.tools.idea.fd.InstantRunBuilder;
-import com.android.tools.idea.fd.InstantRunContext;
-import com.android.tools.idea.fd.InstantRunUtils;
-import com.android.tools.idea.fd.RunAsValidityService;
-import com.android.tools.idea.gradle.run.MakeBeforeRunTaskProvider;
-import com.android.tools.idea.run.AndroidDevice;
-import com.android.tools.idea.run.AndroidRunConfigContext;
-import com.android.tools.idea.run.AndroidSessionInfo;
-import com.android.tools.idea.run.ApkProvisionException;
-import com.android.tools.idea.run.ApplicationIdProvider;
-import com.android.tools.idea.run.DeviceFutures;
-import com.google.common.collect.ImmutableList;
-import com.google.common.collect.Iterables;
-import com.google.common.collect.Lists;
-import com.google.common.util.concurrent.ListenableFuture;
-import com.google.common.util.concurrent.SettableFuture;
-import com.google.idea.blaze.android.manifest.ManifestParser;
-import com.google.idea.blaze.android.run.runner.BlazeAndroidDeviceSelector;
-import com.google.idea.blaze.android.run.runner.BlazeApkBuildStep;
-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.command.BlazeCommand;
-import com.google.idea.blaze.base.command.BlazeCommandName;
-import com.google.idea.blaze.base.command.BlazeFlags;
-import com.google.idea.blaze.base.command.ExperimentalShowArtifactsLineProcessor;
-import com.google.idea.blaze.base.command.info.BlazeInfo;
-import com.google.idea.blaze.base.filecache.FileCaches;
-import com.google.idea.blaze.base.issueparser.IssueOutputLineProcessor;
-import com.google.idea.blaze.base.metrics.Action;
-import com.google.idea.blaze.base.model.primitives.Label;
-import com.google.idea.blaze.base.model.primitives.WorkspaceRoot;
-import com.google.idea.blaze.base.scope.BlazeContext;
-import com.google.idea.blaze.base.scope.ScopedTask;
-import com.google.idea.blaze.base.scope.output.IssueOutput;
-import com.google.idea.blaze.base.scope.scopes.LoggedTimingScope;
-import com.google.idea.blaze.base.settings.Blaze;
-import com.google.idea.blaze.base.util.SaveUtil;
-import com.google.repackaged.devtools.build.lib.rules.android.apkmanifest.ApkManifestOuterClass;
-import com.intellij.execution.Executor;
-import com.intellij.execution.process.ProcessHandler;
-import com.intellij.execution.runners.ExecutionEnvironment;
-import com.intellij.openapi.diagnostic.Logger;
-import com.intellij.openapi.project.Project;
-import java.io.File;
-import java.io.FileInputStream;
-import java.io.IOException;
-import java.io.InputStream;
-import java.io.PrintWriter;
-import java.lang.reflect.InvocationTargetException;
-import java.util.List;
-import java.util.concurrent.CancellationException;
-import java.util.concurrent.ExecutionException;
-import java.util.concurrent.TimeUnit;
-import java.util.concurrent.TimeoutException;
-import javax.annotation.Nullable;
-import org.jetbrains.annotations.NotNull;
-
-/** Builds the APK using normal blaze build. */
-class BlazeApkBuildStepInstantRun implements BlazeApkBuildStep {
-  private static final Logger LOG = Logger.getInstance(BlazeApkBuildStepInstantRun.class);
-
-  private final Project project;
-  private final Executor executor;
-  private final ExecutionEnvironment env;
-  private final Label label;
-  private final ImmutableList<String> buildFlags;
-  private final File instantRunArtifactDirectory;
-  private final File instantRunGradleBuildFile;
-  private final File instantRunBuildInfoFile;
-  private final File instantRunGradlePropertiesFile;
-
-  public static class BuildResult {
-    public final File executionRoot;
-    public final File mergedManifestFile;
-    public final File apkManifestProtoFile;
-    public final ApkManifestOuterClass.ApkManifest apkManifestProto;
-
-    public BuildResult(
-        File executionRoot,
-        File mergedManifestFile,
-        File apkManifestProtoFile,
-        ApkManifestOuterClass.ApkManifest apkManifestProto) {
-      this.executionRoot = executionRoot;
-      this.mergedManifestFile = mergedManifestFile;
-      this.apkManifestProtoFile = apkManifestProtoFile;
-      this.apkManifestProto = apkManifestProto;
-    }
-  }
-
-  private final SettableFuture<BuildResult> buildResultFuture = SettableFuture.create();
-  private final SettableFuture<ApplicationIdProvider> applicationIdProviderFuture =
-      SettableFuture.create();
-  private final SettableFuture<InstantRunContext> instantRunContextFuture = SettableFuture.create();
-  private final SettableFuture<InstantRunBuildAnalyzer> instantRunBuildAnalyzerFuture =
-      SettableFuture.create();
-
-  public BlazeApkBuildStepInstantRun(
-      Project project, ExecutionEnvironment env, Label label, ImmutableList<String> buildFlags) {
-    this.project = project;
-    this.executor = env.getExecutor();
-    this.env = env;
-    this.label = label;
-    this.buildFlags = buildFlags;
-    this.instantRunArtifactDirectory =
-        BlazeInstantRunGradleIntegration.getInstantRunArtifactDirectory(project, label);
-    this.instantRunBuildInfoFile =
-        new File(instantRunArtifactDirectory, "build/reload-dex/debug/build-info.xml");
-    this.instantRunGradleBuildFile = new File(instantRunArtifactDirectory, "build.gradle");
-    this.instantRunGradlePropertiesFile =
-        new File(instantRunArtifactDirectory, "gradle.properties");
-  }
-
-  @Override
-  public boolean build(
-      BlazeContext context, BlazeAndroidDeviceSelector.DeviceSession deviceSession) {
-    if (!instantRunArtifactDirectory.exists() && !instantRunArtifactDirectory.mkdirs()) {
-      IssueOutput.error(
-              "Could not create instant run artifact directory: " + instantRunArtifactDirectory)
-          .submit(context);
-      return false;
-    }
-
-    BuildResult buildResult = buildApkManifest(context);
-    if (buildResult == null) {
-      return false;
-    }
-
-    String gradleUrl = BlazeInstantRunGradleIntegration.getGradleUrl(context);
-    if (gradleUrl == null) {
-      return false;
-    }
-
-    ApplicationIdProvider applicationIdProvider =
-        new BlazeInstantRunApplicationIdProvider(project, buildResult);
-    applicationIdProviderFuture.set(applicationIdProvider);
-
-    // Write build.gradle
-    try (PrintWriter printWriter = new PrintWriter(instantRunGradleBuildFile)) {
-      printWriter.print(
-          BlazeInstantRunGradleIntegration.getGradleBuildInfoString(
-              gradleUrl, buildResult.executionRoot, buildResult.apkManifestProtoFile));
-    } catch (IOException e) {
-      IssueOutput.error("Could not write build.gradle file: " + e).submit(context);
-      return false;
-    }
-
-    // Write gradle.properties
-    try (PrintWriter printWriter = new PrintWriter(instantRunGradlePropertiesFile)) {
-      printWriter.print(BlazeInstantRunGradleIntegration.getGradlePropertiesString());
-    } catch (IOException e) {
-      IssueOutput.error("Could not write build.gradle file: " + e).submit(context);
-      return false;
-    }
-
-    String applicationId = null;
-    try {
-      applicationId = applicationIdProvider.getPackageName();
-    } catch (ApkProvisionException e) {
-      return false;
-    }
-
-    return invokeGradleIrTasks(context, deviceSession, buildResult, applicationId);
-  }
-
-  private BuildResult buildApkManifest(BlazeContext context) {
-    final ScopedTask buildTask =
-        new ScopedTask(context) {
-          @Override
-          protected void execute(@NotNull BlazeContext context) {
-            WorkspaceRoot workspaceRoot = WorkspaceRoot.fromProject(project);
-            String executionRoot = getExecutionRoot(context, workspaceRoot);
-            if (executionRoot == null) {
-              IssueOutput.error("Could not get execution root").submit(context);
-              return;
-            }
-
-            BlazeCommand.Builder command =
-                BlazeCommand.builder(Blaze.getBuildSystem(project), BlazeCommandName.BUILD);
-
-            command
-                .addTargets(label)
-                .addBlazeFlags(buildFlags)
-                .addBlazeFlags("--output_groups=apk_manifest")
-                .addBlazeFlags(BlazeFlags.EXPERIMENTAL_SHOW_ARTIFACTS);
-
-            List<File> apkManifestFiles = Lists.newArrayList();
-
-            SaveUtil.saveAllFiles();
-            int retVal =
-                ExternalTask.builder(workspaceRoot)
-                    .addBlazeCommand(command.build())
-                    .context(context)
-                    .stderr(
-                        LineProcessingOutputStream.of(
-                            new ExperimentalShowArtifactsLineProcessor(
-                                apkManifestFiles, fileName -> fileName.endsWith("apk_manifest")),
-                            new IssueOutputLineProcessor(project, context, workspaceRoot)))
-                    .build()
-                    .run(new LoggedTimingScope(project, Action.BLAZE_BUILD));
-            FileCaches.refresh(project);
-
-            if (retVal != 0) {
-              context.setHasError();
-              return;
-            }
-
-            File apkManifestFile = Iterables.getOnlyElement(apkManifestFiles, null);
-            if (apkManifestFile == null) {
-              IssueOutput.error("Could not find APK manifest file").submit(context);
-              return;
-            }
-
-            ApkManifestOuterClass.ApkManifest apkManifestProto;
-            try (InputStream inputStream = new FileInputStream(apkManifestFile)) {
-              apkManifestProto = ApkManifestOuterClass.ApkManifest.parseFrom(inputStream);
-            } catch (IOException e) {
-              LOG.error(e);
-              IssueOutput.error("Error parsing apk proto").submit(context);
-              return;
-            }
-
-            // Refresh the manifest
-            File mergedManifestFile =
-                new File(executionRoot, apkManifestProto.getAndroidManifest().getExecRootPath());
-            ManifestParser.getInstance(project)
-                .refreshManifests(ImmutableList.of(mergedManifestFile));
-
-            BuildResult buildResult =
-                new BuildResult(
-                    new File(executionRoot), mergedManifestFile, apkManifestFile, apkManifestProto);
-            buildResultFuture.set(buildResult);
-          }
-        };
-
-    BlazeExecutor.submitTask(
-        project,
-        String.format("Executing %s apk build", Blaze.buildSystemName(project)),
-        buildTask);
-
-    try {
-      BuildResult buildResult = buildResultFuture.get();
-      if (!context.shouldContinue()) {
-        return null;
-      }
-      return buildResult;
-    } catch (InterruptedException | ExecutionException e) {
-      context.setHasError();
-    } catch (CancellationException e) {
-      context.setCancelled();
-    }
-    return null;
-  }
-
-  private boolean invokeGradleIrTasks(
-      BlazeContext context,
-      BlazeAndroidDeviceSelector.DeviceSession deviceSession,
-      BuildResult buildResult,
-      String applicationId) {
-    InstantRunContext instantRunContext =
-        new BlazeInstantRunContext(
-            project, buildResult.apkManifestProto, applicationId, instantRunBuildInfoFile);
-    instantRunContextFuture.set(instantRunContext);
-    ProcessHandler previousSessionProcessHandler =
-        deviceSession.sessionInfo != null ? deviceSession.sessionInfo.getProcessHandler() : null;
-    DeviceFutures deviceFutures = deviceSession.deviceFutures;
-    assert deviceFutures != null;
-    List<AndroidDevice> targetDevices = deviceFutures.getDevices();
-    AndroidDevice androidDevice = targetDevices.get(0);
-    IDevice device = getLaunchedDevice(androidDevice);
-
-    AndroidRunConfigContext runConfigContext = new AndroidRunConfigContext();
-    runConfigContext.setTargetDevices(deviceFutures);
-
-    AndroidSessionInfo info = deviceSession.sessionInfo;
-    runConfigContext.setSameExecutorAsPreviousSession(
-        info != null && executor.getId().equals(info.getExecutorId()));
-    runConfigContext.setCleanRerun(InstantRunUtils.isCleanReRun(env));
-
-    InstantRunBuilder instantRunBuilder =
-        new InstantRunBuilder(
-            device,
-            instantRunContext,
-            runConfigContext,
-            new BlazeInstantRunTasksProvider(),
-            RunAsValidityService.getInstance());
-
-    try {
-      List<String> cmdLineArgs = Lists.newArrayList();
-      cmdLineArgs.addAll(MakeBeforeRunTaskProvider.getDeviceSpecificArguments(targetDevices));
-      BlazeInstantRunGradleTaskRunner taskRunner =
-          new BlazeInstantRunGradleTaskRunner(project, context, instantRunGradleBuildFile);
-      boolean success = instantRunBuilder.build(taskRunner, cmdLineArgs);
-      LOG.info("Gradle invocation complete, success = " + success);
-      if (!success) {
-        return false;
-      }
-    } catch (InvocationTargetException e) {
-      LOG.info("Unexpected error while launching gradle before run tasks", e);
-      return false;
-    } catch (InterruptedException e) {
-      LOG.info("Interrupted while launching gradle before run tasks");
-      Thread.currentThread().interrupt();
-      return false;
-    }
-
-    InstantRunBuildAnalyzer analyzer =
-        new InstantRunBuildAnalyzer(project, instantRunContext, previousSessionProcessHandler);
-    instantRunBuildAnalyzerFuture.set(analyzer);
-    return true;
-  }
-
-  ListenableFuture<BuildResult> getBuildResult() {
-    return buildResultFuture;
-  }
-
-  ListenableFuture<ApplicationIdProvider> getApplicationIdProvider() {
-    return applicationIdProviderFuture;
-  }
-
-  ListenableFuture<InstantRunContext> getInstantRunContext() {
-    return instantRunContextFuture;
-  }
-
-  ListenableFuture<InstantRunBuildAnalyzer> getInstantRunBuildAnalyzer() {
-    return instantRunBuildAnalyzerFuture;
-  }
-
-  private String getExecutionRoot(BlazeContext context, WorkspaceRoot workspaceRoot) {
-    ListenableFuture<String> execRootFuture =
-        BlazeInfo.getInstance()
-            .runBlazeInfo(
-                context,
-                Blaze.getBuildSystem(project),
-                workspaceRoot,
-                buildFlags,
-                BlazeInfo.EXECUTION_ROOT_KEY);
-    try {
-      return execRootFuture.get();
-    } catch (InterruptedException e) {
-      context.setCancelled();
-    } catch (ExecutionException e) {
-      LOG.error(e);
-      context.setHasError();
-    }
-    return null;
-  }
-
-  @Nullable
-  private static IDevice getLaunchedDevice(@NotNull AndroidDevice device) {
-    if (!device.getLaunchedDevice().isDone()) {
-      // If we don't have access to the device (this happens if the AVD is still launching)
-      return null;
-    }
-
-    try {
-      return device.getLaunchedDevice().get(1, TimeUnit.MILLISECONDS);
-    } catch (InterruptedException e) {
-      Thread.currentThread().interrupt();
-      return null;
-    } catch (ExecutionException | TimeoutException e) {
-      return null;
-    }
-  }
-}
diff --git a/aswb/src/com/google/idea/blaze/android/run/binary/instantrun/BlazeInstantRunApplicationIdProvider.java b/aswb/src/com/google/idea/blaze/android/run/binary/instantrun/BlazeInstantRunApplicationIdProvider.java
deleted file mode 100644
index ab1461e..0000000
--- a/aswb/src/com/google/idea/blaze/android/run/binary/instantrun/BlazeInstantRunApplicationIdProvider.java
+++ /dev/null
@@ -1,65 +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.run.binary.instantrun;
-
-import com.android.tools.idea.run.ApkProvisionException;
-import com.android.tools.idea.run.ApplicationIdProvider;
-import com.google.idea.blaze.android.manifest.ManifestParser;
-import com.intellij.openapi.application.ApplicationManager;
-import com.intellij.openapi.project.Project;
-import com.intellij.openapi.util.Computable;
-import java.io.File;
-import org.jetbrains.android.dom.manifest.Manifest;
-import org.jetbrains.annotations.NotNull;
-import org.jetbrains.annotations.Nullable;
-
-/** Application id provider for blaze instant run. */
-public class BlazeInstantRunApplicationIdProvider implements ApplicationIdProvider {
-  private final Project project;
-  private final BlazeApkBuildStepInstantRun.BuildResult buildResult;
-
-  public BlazeInstantRunApplicationIdProvider(
-      Project project, BlazeApkBuildStepInstantRun.BuildResult buildResult) {
-    this.project = project;
-    this.buildResult = buildResult;
-  }
-
-  @NotNull
-  @Override
-  public String getPackageName() throws ApkProvisionException {
-    File manifestFile =
-        new File(
-            buildResult.executionRoot,
-            buildResult.apkManifestProto.getAndroidManifest().getExecRootPath());
-    Manifest manifest = ManifestParser.getInstance(project).getManifest(manifestFile);
-    if (manifest == null) {
-      throw new ApkProvisionException("Could not find merged manifest: " + manifestFile);
-    }
-    String applicationId =
-        ApplicationManager.getApplication()
-            .runReadAction((Computable<String>) () -> manifest.getPackage().getValue());
-    if (applicationId == null) {
-      throw new ApkProvisionException("No application id in merged manifest: " + manifestFile);
-    }
-    return applicationId;
-  }
-
-  @Nullable
-  @Override
-  public String getTestPackageName() throws ApkProvisionException {
-    return null;
-  }
-}
diff --git a/aswb/src/com/google/idea/blaze/android/run/binary/instantrun/BlazeInstantRunContext.java b/aswb/src/com/google/idea/blaze/android/run/binary/instantrun/BlazeInstantRunContext.java
deleted file mode 100644
index 9ea7c03..0000000
--- a/aswb/src/com/google/idea/blaze/android/run/binary/instantrun/BlazeInstantRunContext.java
+++ /dev/null
@@ -1,105 +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.run.binary.instantrun;
-
-import com.android.tools.fd.client.InstantRunBuildInfo;
-import com.android.tools.idea.fd.BuildSelection;
-import com.android.tools.idea.fd.FileChangeListener;
-import com.android.tools.idea.fd.InstantRunContext;
-import com.google.common.hash.HashCode;
-import com.google.repackaged.devtools.build.lib.rules.android.apkmanifest.ApkManifestOuterClass;
-import com.intellij.openapi.diagnostic.Logger;
-import com.intellij.openapi.project.Project;
-import java.io.File;
-import java.io.IOException;
-import java.nio.charset.StandardCharsets;
-import java.nio.file.Files;
-import java.nio.file.Paths;
-import org.jetbrains.annotations.NotNull;
-import org.jetbrains.annotations.Nullable;
-
-/** Blaze implementation of instant run context. */
-public class BlazeInstantRunContext implements InstantRunContext {
-  private static final Logger LOG = Logger.getInstance(BlazeInstantRunContext.class);
-  private final Project project;
-  private final ApkManifestOuterClass.ApkManifest apkManifest;
-  private final String applicationId;
-  private final File instantRunBuildInfoFile;
-  private BuildSelection buildSelection;
-
-  BlazeInstantRunContext(
-      Project project,
-      ApkManifestOuterClass.ApkManifest apkManifest,
-      String applicationId,
-      File instantRunBuildInfoFile) {
-    this.project = project;
-    this.apkManifest = apkManifest;
-    this.applicationId = applicationId;
-    this.instantRunBuildInfoFile = instantRunBuildInfoFile;
-  }
-
-  @NotNull
-  @Override
-  public String getApplicationId() {
-    return applicationId;
-  }
-
-  @NotNull
-  @Override
-  public HashCode getManifestResourcesHash() {
-    // TODO b/28373160
-    return HashCode.fromInt(0);
-  }
-
-  @Override
-  public boolean usesMultipleProcesses() {
-    // TODO(tomlu) -- does this make sense in blaze? We can of course just parse the manifest.
-    return false;
-  }
-
-  @Nullable
-  @Override
-  public FileChangeListener.Changes getFileChangesAndReset() {
-    return null;
-  }
-
-  @Nullable
-  @Override
-  public InstantRunBuildInfo getInstantRunBuildInfo() {
-    if (instantRunBuildInfoFile.exists()) {
-      try {
-        String xml =
-            new String(
-                Files.readAllBytes(Paths.get(instantRunBuildInfoFile.getPath())),
-                StandardCharsets.UTF_8);
-        return InstantRunBuildInfo.get(xml);
-      } catch (IOException e) {
-        LOG.error(e);
-      }
-    }
-    return null;
-  }
-
-  @Override
-  public void setBuildSelection(@NotNull BuildSelection buildSelection) {
-    this.buildSelection = buildSelection;
-  }
-
-  @Override
-  public BuildSelection getBuildSelection() {
-    return buildSelection;
-  }
-}
diff --git a/aswb/src/com/google/idea/blaze/android/run/binary/instantrun/BlazeInstantRunDeviceSelector.java b/aswb/src/com/google/idea/blaze/android/run/binary/instantrun/BlazeInstantRunDeviceSelector.java
deleted file mode 100644
index 8c26fc4..0000000
--- a/aswb/src/com/google/idea/blaze/android/run/binary/instantrun/BlazeInstantRunDeviceSelector.java
+++ /dev/null
@@ -1,96 +0,0 @@
-/*
- * Copyright (C) 2016 The Android Open Source Project
- *
- * 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.run.binary.instantrun;
-
-import com.android.ddmlib.IDevice;
-import com.android.tools.idea.fd.InstantRunManager;
-import com.android.tools.idea.fd.InstantRunUtils;
-import com.android.tools.idea.run.AndroidSessionInfo;
-import com.android.tools.idea.run.DeviceFutures;
-import com.google.idea.blaze.android.run.runner.BlazeAndroidDeviceSelector;
-import com.google.idea.blaze.android.run.runner.BlazeAndroidRunConfigurationDeployTargetManager;
-import com.intellij.execution.ExecutionException;
-import com.intellij.execution.Executor;
-import com.intellij.execution.runners.ExecutionEnvironment;
-import com.intellij.openapi.project.Project;
-import java.util.List;
-import javax.annotation.Nullable;
-import org.jetbrains.android.facet.AndroidFacet;
-
-/** Tries to reuse devices from a previous session. */
-public class BlazeInstantRunDeviceSelector implements BlazeAndroidDeviceSelector {
-  NormalDeviceSelector normalDeviceSelector = new NormalDeviceSelector();
-
-  @Override
-  public DeviceSession getDevice(
-      Project project,
-      AndroidFacet facet,
-      BlazeAndroidRunConfigurationDeployTargetManager deployTargetManager,
-      Executor executor,
-      ExecutionEnvironment env,
-      AndroidSessionInfo info,
-      boolean debug,
-      int runConfigId)
-      throws ExecutionException {
-    DeviceFutures deviceFutures = null;
-    if (info != null) {
-      // if there is an existing previous session,
-      // then see if we can detect devices to fast deploy to
-      deviceFutures = getFastDeployDevices(executor, info);
-
-      if (InstantRunUtils.isReRun(env)) {
-        info.getProcessHandler().destroyProcess();
-        info = null;
-      }
-    }
-
-    if (deviceFutures != null) {
-      return new DeviceSession(null, deviceFutures, info);
-    }
-
-    // Fall back to normal device selection
-    return normalDeviceSelector.getDevice(
-        project, facet, deployTargetManager, executor, env, info, debug, runConfigId);
-  }
-
-  @Nullable
-  private static DeviceFutures getFastDeployDevices(Executor executor, AndroidSessionInfo info) {
-    if (!info.getExecutorId().equals(executor.getId())) {
-      String msg =
-          String.format(
-              "Cannot Instant Run since old executor (%1$s) doesn't match current executor (%2$s)",
-              info.getExecutorId(), executor.getId());
-      InstantRunManager.LOG.info(msg);
-      return null;
-    }
-
-    List<IDevice> devices = info.getDevices();
-    if (devices == null || devices.isEmpty()) {
-      InstantRunManager.LOG.info(
-          "Cannot Instant Run since we could not locate "
-              + "the devices from the existing launch session");
-      return null;
-    }
-
-    if (devices.size() > 1) {
-      InstantRunManager.LOG.info(
-          "Last run was on > 1 device, not reusing devices and prompting again");
-      return null;
-    }
-
-    return DeviceFutures.forDevices(devices);
-  }
-}
diff --git a/aswb/src/com/google/idea/blaze/android/run/binary/instantrun/BlazeInstantRunGradleIntegration.java b/aswb/src/com/google/idea/blaze/android/run/binary/instantrun/BlazeInstantRunGradleIntegration.java
deleted file mode 100644
index a9873ca..0000000
--- a/aswb/src/com/google/idea/blaze/android/run/binary/instantrun/BlazeInstantRunGradleIntegration.java
+++ /dev/null
@@ -1,126 +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.run.binary.instantrun;
-
-import com.android.SdkConstants;
-import com.google.common.base.Joiner;
-import com.google.common.hash.Hashing;
-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.model.primitives.Label;
-import com.google.idea.blaze.base.scope.BlazeContext;
-import com.google.idea.blaze.base.scope.output.IssueOutput;
-import com.google.idea.blaze.base.scope.output.StatusOutput;
-import com.google.idea.blaze.base.settings.BlazeImportSettings;
-import com.google.idea.blaze.base.settings.BlazeImportSettingsManager;
-import com.google.idea.blaze.base.sync.data.BlazeDataStorage;
-import com.google.idea.common.experiments.DeveloperFlag;
-import com.google.idea.common.experiments.StringExperiment;
-import com.intellij.openapi.application.PathManager;
-import com.intellij.openapi.project.Project;
-import java.io.File;
-import javax.annotation.Nullable;
-
-/** Defines where instant run storage and artifacts go. */
-class BlazeInstantRunGradleIntegration {
-  private static final String INSTANT_RUN_SUBDIRECTORY = "instantrun";
-
-  private static final StringExperiment LOCAL_GRADLE_VERSION =
-      new StringExperiment("use.local.gradle.version");
-  private static final DeveloperFlag REBUILD_LOCAL_GRADLE =
-      new DeveloperFlag("rebuild.local.gradle");
-
-  /** Gets a unique directory for a given target that can be used for the build process. */
-  static File getInstantRunArtifactDirectory(Project project, Label target) {
-    BlazeImportSettings importSettings =
-        BlazeImportSettingsManager.getInstance(project).getImportSettings();
-    assert importSettings != null;
-    File dataSubDirectory = BlazeDataStorage.getProjectDataDir(importSettings);
-    File instantRunDirectory = new File(dataSubDirectory, INSTANT_RUN_SUBDIRECTORY);
-    String targetHash = Hashing.md5().hashUnencodedChars(target.toString()).toString();
-    return new File(instantRunDirectory, targetHash);
-  }
-
-  @Nullable
-  static String getGradleUrl(BlazeContext context) {
-    String localGradleVersion = LOCAL_GRADLE_VERSION.getValue();
-    boolean isDevMode = localGradleVersion != null;
-
-    if (isDevMode) {
-      String toolsIdeaPath = PathManager.getHomePath();
-      File toolsDir = new File(toolsIdeaPath).getParentFile();
-      File repoDir = toolsDir.getParentFile();
-      File localGradleDirectory =
-          new File(
-              new File(repoDir, "out/repo/com/android/tools/build/builder"), localGradleVersion);
-      if (REBUILD_LOCAL_GRADLE.getValue() || !localGradleDirectory.exists()) {
-        // Build gradle
-        context.output(new StatusOutput("Building local Gradle..."));
-        int retVal =
-            ExternalTask.builder(toolsDir)
-                .args("./gradlew", ":init", ":publishLocal")
-                .stdout(LineProcessingOutputStream.of(new PrintOutputLineProcessor(context)))
-                .build()
-                .run();
-
-        if (retVal != 0) {
-          IssueOutput.error("Gradle build failed.").submit(context);
-          return null;
-        }
-      }
-      return new File(repoDir, "out/repo").getPath();
-    }
-
-    // Not supported yet
-    IssueOutput.error(
-            "You must specify 'use.local.gradle.version' experiment, "
-                + "non-local gradle not supported yet.")
-        .submit(context);
-    return null;
-  }
-
-  static String getGradlePropertiesString() {
-    return Joiner.on('\n')
-        .join("org.gradle.daemon=true", "org.gradle.jvmargs=-XX:MaxPermSize=1024m -Xmx4096m");
-  }
-
-  static String getGradleBuildInfoString(
-      String gradleUrl, File executionRoot, File apkManifestFile) {
-    String template =
-        Joiner.on('\n')
-            .join(
-                "buildscript {",
-                "  repositories {",
-                "    jcenter()",
-                "    maven { url '%s' }",
-                "  }",
-                "  dependencies {",
-                "    classpath 'com.android.tools.build:gradle:%s'",
-                "  }",
-                "}",
-                "apply plugin: 'com.android.external.build'",
-                "externalBuild {",
-                "  executionRoot = '%s'",
-                "  buildManifestPath = '%s'",
-                "}");
-    String gradleVersion = LOCAL_GRADLE_VERSION.getValue();
-    gradleVersion = gradleVersion != null ? gradleVersion : SdkConstants.GRADLE_LATEST_VERSION;
-
-    return String.format(
-        template, gradleUrl, gradleVersion, executionRoot.getPath(), apkManifestFile.getPath());
-  }
-}
diff --git a/aswb/src/com/google/idea/blaze/android/run/binary/instantrun/BlazeInstantRunGradleTaskRunner.java b/aswb/src/com/google/idea/blaze/android/run/binary/instantrun/BlazeInstantRunGradleTaskRunner.java
deleted file mode 100644
index bdeab5a..0000000
--- a/aswb/src/com/google/idea/blaze/android/run/binary/instantrun/BlazeInstantRunGradleTaskRunner.java
+++ /dev/null
@@ -1,130 +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.run.binary.instantrun;
-
-import static com.android.tools.idea.gradle.util.GradleUtil.GRADLE_SYSTEM_ID;
-import static com.intellij.openapi.externalSystem.model.task.ExternalSystemTaskType.EXECUTE_TASK;
-
-import com.android.builder.model.AndroidProject;
-import com.android.tools.idea.gradle.invoker.GradleInvocationResult;
-import com.android.tools.idea.gradle.invoker.GradleInvoker;
-import com.android.tools.idea.gradle.run.GradleTaskRunner;
-import com.android.tools.idea.gradle.util.AndroidGradleSettings;
-import com.android.tools.idea.gradle.util.BuildMode;
-import com.google.common.base.Strings;
-import com.google.common.collect.ImmutableList;
-import com.google.idea.blaze.base.scope.BlazeContext;
-import com.google.idea.blaze.base.scope.output.PrintOutput;
-import com.intellij.openapi.application.ApplicationManager;
-import com.intellij.openapi.externalSystem.model.task.ExternalSystemTaskId;
-import com.intellij.openapi.externalSystem.model.task.ExternalSystemTaskNotificationListenerAdapter;
-import com.intellij.openapi.project.Project;
-import com.intellij.util.concurrency.Semaphore;
-import java.io.File;
-import java.lang.reflect.InvocationTargetException;
-import java.util.ArrayList;
-import java.util.List;
-import java.util.concurrent.atomic.AtomicBoolean;
-import javax.swing.SwingUtilities;
-import org.jetbrains.annotations.NotNull;
-import org.jetbrains.annotations.Nullable;
-
-class BlazeInstantRunGradleTaskRunner implements GradleTaskRunner {
-  private final Project project;
-  private final BlazeContext context;
-  private final File instantRunGradleBuildFile;
-
-  public BlazeInstantRunGradleTaskRunner(
-      Project project, BlazeContext context, File instantRunGradleBuildFile) {
-    this.project = project;
-    this.context = context;
-    this.instantRunGradleBuildFile = instantRunGradleBuildFile;
-  }
-
-  @Override
-  public boolean run(
-      @NotNull List<String> tasks,
-      @Nullable BuildMode buildMode,
-      @NotNull List<String> commandLineArguments)
-      throws InvocationTargetException, InterruptedException {
-    assert !ApplicationManager.getApplication().isDispatchThread();
-
-    final GradleInvoker gradleInvoker = GradleInvoker.getInstance(project);
-
-    final AtomicBoolean success = new AtomicBoolean();
-    final Semaphore done = new Semaphore();
-    done.down();
-
-    final GradleInvoker.AfterGradleInvocationTask afterTask =
-        new GradleInvoker.AfterGradleInvocationTask() {
-          @Override
-          public void execute(@NotNull GradleInvocationResult result) {
-            success.set(result.isBuildSuccessful());
-            gradleInvoker.removeAfterGradleInvocationTask(this);
-            done.up();
-          }
-        };
-
-    ExternalSystemTaskId taskId =
-        ExternalSystemTaskId.create(GRADLE_SYSTEM_ID, EXECUTE_TASK, project);
-    List<String> jvmArguments = ImmutableList.of();
-
-    // https://code.google.com/p/android/issues/detail?id=213040 -
-    // make split apks only available if an env var is set
-    List<String> args = new ArrayList<>(commandLineArguments);
-    if (!Boolean.valueOf(System.getenv(GradleTaskRunner.USE_SPLIT_APK))) {
-      // force multi dex when the env var is not set to true
-      args.add(
-          AndroidGradleSettings.createProjectProperty(
-              AndroidProject.PROPERTY_SIGNING_COLDSWAP_MODE, "MULTIDEX"));
-    }
-
-    // To ensure that the "Run Configuration" waits for the Gradle tasks to be executed,
-    // we use SwingUtilities.invokeAndWait. I tried
-    // using Application.invokeAndWait but it never worked.
-    // IDEA also uses SwingUtilities in this scenario (see CompileStepBeforeRun.)
-    SwingUtilities.invokeAndWait(
-        () -> {
-          gradleInvoker.addAfterGradleInvocationTask(afterTask);
-          gradleInvoker.executeTasks(
-              tasks,
-              jvmArguments,
-              args,
-              taskId,
-              new GradleNotificationListener(),
-              instantRunGradleBuildFile,
-              false,
-              true);
-        });
-
-    done.waitFor();
-    return success.get();
-  }
-
-  class GradleNotificationListener extends ExternalSystemTaskNotificationListenerAdapter {
-    @Override
-    public void onTaskOutput(
-        @NotNull ExternalSystemTaskId id, @NotNull String text, boolean stdOut) {
-      super.onTaskOutput(id, text, stdOut);
-      String toPrint = text.trim();
-      if (!Strings.isNullOrEmpty(toPrint)) {
-        context.output(
-            new PrintOutput(
-                toPrint, stdOut ? PrintOutput.OutputType.NORMAL : PrintOutput.OutputType.ERROR));
-      }
-    }
-  }
-}
diff --git a/aswb/src/com/google/idea/blaze/android/run/binary/instantrun/BlazeInstantRunTasksProvider.java b/aswb/src/com/google/idea/blaze/android/run/binary/instantrun/BlazeInstantRunTasksProvider.java
deleted file mode 100644
index 52a82f7..0000000
--- a/aswb/src/com/google/idea/blaze/android/run/binary/instantrun/BlazeInstantRunTasksProvider.java
+++ /dev/null
@@ -1,36 +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.run.binary.instantrun;
-
-import com.android.tools.idea.fd.InstantRunTasksProvider;
-import com.google.common.collect.ImmutableList;
-import java.util.List;
-import org.jetbrains.annotations.NotNull;
-
-/** Returns blaze-specific instant run tasks. */
-public class BlazeInstantRunTasksProvider implements InstantRunTasksProvider {
-  @NotNull
-  @Override
-  public List<String> getCleanAndGenerateSourcesTasks() {
-    return ImmutableList.of();
-  }
-
-  @NotNull
-  @Override
-  public List<String> getFullBuildTasks() {
-    return ImmutableList.of("process");
-  }
-}
diff --git a/aswb/src/com/google/idea/blaze/android/run/binary/mobileinstall/BlazeAndroidBinaryMobileInstallRunContext.java b/aswb/src/com/google/idea/blaze/android/run/binary/mobileinstall/BlazeAndroidBinaryMobileInstallRunContext.java
index a25eba9..2ea7ebc 100644
--- a/aswb/src/com/google/idea/blaze/android/run/binary/mobileinstall/BlazeAndroidBinaryMobileInstallRunContext.java
+++ b/aswb/src/com/google/idea/blaze/android/run/binary/mobileinstall/BlazeAndroidBinaryMobileInstallRunContext.java
@@ -30,6 +30,7 @@
 import com.android.tools.idea.run.util.ProcessHandlerLaunchStatus;
 import com.google.common.collect.ImmutableList;
 import com.google.common.util.concurrent.Futures;
+import com.google.idea.blaze.android.compatibility.Compatibility;
 import com.google.idea.blaze.android.run.binary.BlazeAndroidBinaryApplicationIdProvider;
 import com.google.idea.blaze.android.run.binary.BlazeAndroidBinaryApplicationLaunchTaskProvider;
 import com.google.idea.blaze.android.run.binary.BlazeAndroidBinaryConsoleProvider;
@@ -159,14 +160,22 @@
 
   @Nullable
   @Override
+  @SuppressWarnings("unchecked")
   public DebugConnectorTask getDebuggerTask(
       AndroidDebugger androidDebugger,
       AndroidDebuggerState androidDebuggerState,
-      Set<String> packageIds)
+      Set<String> packageIds,
+      boolean monitorRemoteProcess)
       throws ExecutionException {
-    //noinspection unchecked
-    return androidDebugger.getConnectDebuggerTask(
-        env, null, packageIds, facet, androidDebuggerState, runConfiguration.getType().getId());
+    return Compatibility.getConnectDebuggerTask(
+        androidDebugger,
+        env,
+        null,
+        packageIds,
+        facet,
+        androidDebuggerState,
+        runConfiguration.getType().getId(),
+        monitorRemoteProcess);
   }
 
   @Nullable
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 24d49b6..1256ff4 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,6 +23,7 @@
 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;
@@ -56,8 +57,6 @@
 import java.nio.file.Paths;
 import java.util.concurrent.CancellationException;
 import javax.annotation.Nullable;
-import org.jetbrains.android.sdk.AndroidSdkUtils;
-import org.jetbrains.annotations.NotNull;
 
 /** Builds and installs the APK using mobile-install. */
 public class BlazeApkBuildStepMobileInstall implements BlazeApkBuildStep {
@@ -89,7 +88,7 @@
     final ScopedTask buildTask =
         new ScopedTask(context) {
           @Override
-          protected void execute(@NotNull BlazeContext context) {
+          protected void execute(BlazeContext context) {
             boolean incrementalInstall = env.getExecutor() instanceof IncrementalInstallExecutor;
 
             DeviceFutures deviceFutures = deviceSession.deviceFutures;
@@ -203,8 +202,7 @@
   }
 
   @Nullable
-  private static IDevice resolveDevice(
-      @NotNull BlazeContext context, @NotNull DeviceFutures deviceFutures) {
+  private static IDevice resolveDevice(BlazeContext context, DeviceFutures deviceFutures) {
     if (deviceFutures.get().size() != 1) {
       IssueOutput.error("Only one device can be used with mobile-install.").submit(context);
       return null;
diff --git a/aswb/src/com/google/idea/blaze/android/run/deployinfo/BlazeAndroidDeployInfo.java b/aswb/src/com/google/idea/blaze/android/run/deployinfo/BlazeAndroidDeployInfo.java
index e5f5ad3..afb7909 100644
--- a/aswb/src/com/google/idea/blaze/android/run/deployinfo/BlazeAndroidDeployInfo.java
+++ b/aswb/src/com/google/idea/blaze/android/run/deployinfo/BlazeAndroidDeployInfo.java
@@ -18,7 +18,9 @@
 import com.google.common.collect.Lists;
 import com.google.idea.blaze.android.manifest.ManifestParser;
 import com.google.repackaged.devtools.build.lib.rules.android.deployinfo.AndroidDeployInfoOuterClass;
+import com.intellij.openapi.application.ApplicationManager;
 import com.intellij.openapi.project.Project;
+import com.intellij.openapi.util.Computable;
 import java.io.File;
 import java.util.List;
 import java.util.Objects;
@@ -49,7 +51,11 @@
   @Nullable
   public Manifest getMergedManifest() {
     File manifestFile = getMergedManifestFile();
-    return ManifestParser.getInstance(project).getManifest(manifestFile);
+    // Run in a read action since otherwise, it might throw a read access exception.
+    return ApplicationManager.getApplication()
+        .runReadAction(
+            (Computable<Manifest>)
+                () -> ManifestParser.getInstance(project).getManifest(manifestFile));
   }
 
   public List<File> getAdditionalMergedManifestFiles() {
diff --git a/aswb/src/com/google/idea/blaze/android/run/runner/BlazeAndroidLaunchTasksProvider.java b/aswb/src/com/google/idea/blaze/android/run/runner/BlazeAndroidLaunchTasksProvider.java
index 7354b25..dacc20b 100644
--- a/aswb/src/com/google/idea/blaze/android/run/runner/BlazeAndroidLaunchTasksProvider.java
+++ b/aswb/src/com/google/idea/blaze/android/run/runner/BlazeAndroidLaunchTasksProvider.java
@@ -169,7 +169,8 @@
     }
 
     try {
-      return runContext.getDebuggerTask(androidDebugger, androidDebuggerState, packageIds);
+      return runContext.getDebuggerTask(
+          androidDebugger, androidDebuggerState, packageIds, monitorRemoteProcess());
     } catch (ExecutionException e) {
       launchStatus.terminateLaunch(e.getMessage());
       return null;
diff --git a/aswb/src/com/google/idea/blaze/android/run/runner/BlazeAndroidRunConfigurationRunner.java b/aswb/src/com/google/idea/blaze/android/run/runner/BlazeAndroidRunConfigurationRunner.java
index 76e3bfc..6d8feb4 100644
--- a/aswb/src/com/google/idea/blaze/android/run/runner/BlazeAndroidRunConfigurationRunner.java
+++ b/aswb/src/com/google/idea/blaze/android/run/runner/BlazeAndroidRunConfigurationRunner.java
@@ -36,6 +36,7 @@
 import com.google.idea.blaze.base.experiments.ExperimentScope;
 import com.google.idea.blaze.base.metrics.Action;
 import com.google.idea.blaze.base.run.confighandler.BlazeCommandRunConfigurationRunner;
+import com.google.idea.blaze.base.run.smrunner.SmRunnerUtils;
 import com.google.idea.blaze.base.scope.Scope;
 import com.google.idea.blaze.base.scope.output.IssueOutput;
 import com.google.idea.blaze.base.scope.scopes.BlazeConsoleScope;
@@ -79,7 +80,7 @@
 
   private static final Key<BlazeAndroidRunContext> RUN_CONTEXT_KEY =
       Key.create("blaze.run.context");
-  private static final Key<BlazeAndroidDeviceSelector.DeviceSession> DEVICE_SESSION_KEY =
+  public static final Key<BlazeAndroidDeviceSelector.DeviceSession> DEVICE_SESSION_KEY =
       Key.create("blaze.device.session");
 
   private final Module module;
@@ -251,7 +252,17 @@
 
     @Nullable
     @Override
-    public ExecutionResult execute(Executor executor, @NotNull ProgramRunner runner)
+    public ExecutionResult execute(Executor executor, ProgramRunner runner)
+        throws ExecutionException {
+      DefaultExecutionResult result = executeInner(executor, runner);
+      if (result == null) {
+        return null;
+      }
+      return SmRunnerUtils.attachRerunFailedTestsAction(result);
+    }
+
+    @Nullable
+    private DefaultExecutionResult executeInner(Executor executor, @NotNull ProgramRunner<?> runner)
         throws ExecutionException {
       ProcessHandler processHandler;
       ConsoleView console;
diff --git a/aswb/src/com/google/idea/blaze/android/run/runner/BlazeAndroidRunContext.java b/aswb/src/com/google/idea/blaze/android/run/runner/BlazeAndroidRunContext.java
index 1778a63..a0653a1 100644
--- a/aswb/src/com/google/idea/blaze/android/run/runner/BlazeAndroidRunContext.java
+++ b/aswb/src/com/google/idea/blaze/android/run/runner/BlazeAndroidRunContext.java
@@ -72,7 +72,8 @@
   DebugConnectorTask getDebuggerTask(
       AndroidDebugger androidDebugger,
       AndroidDebuggerState androidDebuggerState,
-      Set<String> packageIds)
+      Set<String> packageIds,
+      boolean monitorRemoteProcess)
       throws ExecutionException;
 
   @Nullable
diff --git a/aswb/src/com/google/idea/blaze/android/run/test/AndroidTestConsoleProvider.java b/aswb/src/com/google/idea/blaze/android/run/test/AndroidTestConsoleProvider.java
index 56ff338..d91419a 100644
--- a/aswb/src/com/google/idea/blaze/android/run/test/AndroidTestConsoleProvider.java
+++ b/aswb/src/com/google/idea/blaze/android/run/test/AndroidTestConsoleProvider.java
@@ -16,10 +16,13 @@
 package com.google.idea.blaze.android.run.test;
 
 import com.android.tools.idea.run.ConsoleProvider;
-import com.android.tools.idea.run.testing.AndroidTestConsoleProperties;
+import com.google.idea.blaze.android.compatibility.Compatibility.AndroidTestConsoleProperties;
+import com.google.idea.blaze.android.run.test.smrunner.BlazeAndroidTestEventsHandler;
+import com.google.idea.blaze.base.run.smrunner.SmRunnerUtils;
 import com.intellij.execution.ExecutionException;
 import com.intellij.execution.Executor;
 import com.intellij.execution.configurations.RunConfiguration;
+import com.intellij.execution.executors.DefaultDebugExecutor;
 import com.intellij.execution.filters.TextConsoleBuilderFactory;
 import com.intellij.execution.process.ProcessHandler;
 import com.intellij.execution.testframework.sm.SMTestRunnerConnectionUtil;
@@ -27,37 +30,50 @@
 import com.intellij.openapi.Disposable;
 import com.intellij.openapi.project.Project;
 import com.intellij.openapi.util.Disposer;
-import org.jetbrains.annotations.NotNull;
+import javax.annotation.Nullable;
 
 /** Console provider for android_test */
 class AndroidTestConsoleProvider implements ConsoleProvider {
   private final Project project;
   private final RunConfiguration runConfiguration;
   private final BlazeAndroidTestRunConfigurationState configState;
+  @Nullable private final BlazeAndroidTestEventsHandler testEventsHandler;
 
   AndroidTestConsoleProvider(
       Project project,
       RunConfiguration runConfiguration,
-      BlazeAndroidTestRunConfigurationState configState) {
+      BlazeAndroidTestRunConfigurationState configState,
+      @Nullable BlazeAndroidTestEventsHandler testEventsHandler) {
     this.project = project;
     this.runConfiguration = runConfiguration;
     this.configState = configState;
+    this.testEventsHandler = testEventsHandler;
   }
 
-  @NotNull
   @Override
-  public ConsoleView createAndAttach(
-      @NotNull Disposable parent, @NotNull ProcessHandler handler, @NotNull Executor executor)
+  public ConsoleView createAndAttach(Disposable parent, ProcessHandler handler, Executor executor)
       throws ExecutionException {
     if (!configState.isRunThroughBlaze()) {
       return getStockConsoleProvider().createAndAttach(parent, handler, executor);
     }
-    ConsoleView console =
-        TextConsoleBuilderFactory.getInstance().createBuilder(project).getConsole();
+    ConsoleView console = createBlazeTestConsole(executor);
     console.attachToProcess(handler);
     return console;
   }
 
+  private ConsoleView createBlazeTestConsole(Executor executor) {
+    if (testEventsHandler == null || isDebugging(executor)) {
+      // SM runner console not yet supported when debugging, because we're calling this once per
+      // test case (see ConnectBlazeTestDebuggerTask::setUpForReattachingDebugger)
+      return TextConsoleBuilderFactory.getInstance().createBuilder(project).getConsole();
+    }
+    return SmRunnerUtils.getConsoleView(project, runConfiguration, executor, testEventsHandler);
+  }
+
+  private static boolean isDebugging(Executor executor) {
+    return executor instanceof DefaultDebugExecutor;
+  }
+
   private ConsoleProvider getStockConsoleProvider() {
     return (parent, handler, executor) -> {
       AndroidTestConsoleProperties properties =
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 64e8239..8d08ec0 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
@@ -15,8 +15,8 @@
  */
 package com.google.idea.blaze.android.run.test;
 
-import com.android.tools.idea.run.testing.AndroidTestRunConfiguration;
 import com.google.common.base.Strings;
+import com.google.idea.blaze.android.compatibility.Compatibility.AndroidTestRunConfiguration;
 import com.google.idea.blaze.base.ideinfo.TargetIdeInfo;
 import com.google.idea.blaze.base.model.primitives.Kind;
 import com.google.idea.blaze.base.run.BlazeCommandRunConfiguration;
diff --git a/aswb/src/com/google/idea/blaze/android/run/test/BlazeAndroidTestFilter.java b/aswb/src/com/google/idea/blaze/android/run/test/BlazeAndroidTestFilter.java
index c6f4a3b..e5f2b1d 100644
--- a/aswb/src/com/google/idea/blaze/android/run/test/BlazeAndroidTestFilter.java
+++ b/aswb/src/com/google/idea/blaze/android/run/test/BlazeAndroidTestFilter.java
@@ -15,8 +15,8 @@
  */
 package com.google.idea.blaze.android.run.test;
 
-import com.android.tools.idea.run.testing.AndroidTestRunConfiguration;
 import com.google.common.collect.ImmutableList;
+import com.google.idea.blaze.android.compatibility.Compatibility.AndroidTestRunConfiguration;
 import com.google.idea.blaze.base.command.BlazeFlags;
 import org.jetbrains.annotations.NotNull;
 import org.jetbrains.annotations.Nullable;
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 0c42ea9..e37f07f 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
@@ -15,8 +15,8 @@
  */
 package com.google.idea.blaze.android.run.test;
 
-import com.android.tools.idea.run.testing.AndroidTestRunConfiguration;
 import com.google.common.base.Strings;
+import com.google.idea.blaze.android.compatibility.Compatibility.AndroidTestRunConfiguration;
 import com.google.idea.blaze.base.ideinfo.TargetIdeInfo;
 import com.google.idea.blaze.base.model.primitives.Kind;
 import com.google.idea.blaze.base.run.BlazeCommandRunConfiguration;
diff --git a/aswb/src/com/google/idea/blaze/android/run/test/BlazeAndroidTestRunConfigurationStateEditor.java b/aswb/src/com/google/idea/blaze/android/run/test/BlazeAndroidTestRunConfigurationStateEditor.java
index eb8293a..5df4512 100644
--- a/aswb/src/com/google/idea/blaze/android/run/test/BlazeAndroidTestRunConfigurationStateEditor.java
+++ b/aswb/src/com/google/idea/blaze/android/run/test/BlazeAndroidTestRunConfigurationStateEditor.java
@@ -16,10 +16,10 @@
 
 package com.google.idea.blaze.android.run.test;
 
-import static com.android.tools.idea.run.testing.AndroidTestRunConfiguration.TEST_ALL_IN_MODULE;
-import static com.android.tools.idea.run.testing.AndroidTestRunConfiguration.TEST_ALL_IN_PACKAGE;
-import static com.android.tools.idea.run.testing.AndroidTestRunConfiguration.TEST_CLASS;
-import static com.android.tools.idea.run.testing.AndroidTestRunConfiguration.TEST_METHOD;
+import static com.google.idea.blaze.android.compatibility.Compatibility.AndroidTestRunConfiguration.TEST_ALL_IN_MODULE;
+import static com.google.idea.blaze.android.compatibility.Compatibility.AndroidTestRunConfiguration.TEST_ALL_IN_PACKAGE;
+import static com.google.idea.blaze.android.compatibility.Compatibility.AndroidTestRunConfiguration.TEST_CLASS;
+import static com.google.idea.blaze.android.compatibility.Compatibility.AndroidTestRunConfiguration.TEST_METHOD;
 
 import com.google.idea.blaze.base.run.state.RunConfigurationState;
 import com.google.idea.blaze.base.run.state.RunConfigurationStateEditor;
@@ -64,6 +64,8 @@
   private JCheckBox runThroughBlazeTestCheckBox;
   private final JRadioButton[] testingType2RadioButton = new JRadioButton[4];
 
+  private boolean componentEnabled = true;
+
   BlazeAndroidTestRunConfigurationStateEditor(
       RunConfigurationStateEditor commonStateEditor, Project project) {
     this.commonStateEditor = commonStateEditor;
@@ -478,4 +480,29 @@
   public JComponent createComponent() {
     return UiUtil.createBox(commonStateEditor.createComponent(), panel);
   }
+
+  @Override
+  public void setComponentEnabled(boolean enabled) {
+    componentEnabled = enabled;
+    updateEnabledState();
+  }
+
+  private void updateEnabledState() {
+    commonStateEditor.setComponentEnabled(componentEnabled);
+    allInPackageButton.setEnabled(componentEnabled);
+    classButton.setEnabled(componentEnabled);
+    testMethodButton.setEnabled(componentEnabled);
+    allInTargetButton.setEnabled(componentEnabled);
+    packageComponent.setEnabled(componentEnabled);
+    classComponent.setEnabled(componentEnabled);
+    methodComponent.setEnabled(componentEnabled);
+    runnerComponent.setEnabled(componentEnabled);
+    labelTest.setEnabled(componentEnabled);
+    runThroughBlazeTestCheckBox.setEnabled(componentEnabled);
+    for (JComponent button : testingType2RadioButton) {
+      if (button != null) {
+        button.setEnabled(componentEnabled);
+      }
+    }
+  }
 }
diff --git a/aswb/src/com/google/idea/blaze/android/run/test/BlazeAndroidTestRunConfigurationType.java b/aswb/src/com/google/idea/blaze/android/run/test/BlazeAndroidTestRunConfigurationType.java
deleted file mode 100644
index 2104d75..0000000
--- a/aswb/src/com/google/idea/blaze/android/run/test/BlazeAndroidTestRunConfigurationType.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.android.run.test;
-
-import com.google.idea.blaze.base.run.BlazeCommandRunConfiguration;
-import com.google.idea.blaze.base.run.BlazeCommandRunConfigurationType;
-import com.google.idea.blaze.base.settings.Blaze;
-import com.intellij.execution.BeforeRunTask;
-import com.intellij.execution.configurations.ConfigurationFactory;
-import com.intellij.execution.configurations.ConfigurationType;
-import com.intellij.execution.configurations.ConfigurationTypeUtil;
-import com.intellij.execution.configurations.UnknownConfigurationType;
-import com.intellij.icons.AllIcons;
-import com.intellij.openapi.project.Project;
-import com.intellij.openapi.util.Key;
-import com.intellij.ui.LayeredIcon;
-import icons.AndroidIcons;
-import javax.swing.Icon;
-import org.jetbrains.annotations.NotNull;
-
-/**
- * A type for Android test run configurations adapted specifically to run android_test targets.
- *
- * @deprecated See {@link com.google.idea.blaze.base.run.BlazeCommandRunConfigurationType}. Retained
- *     in 1.9 for legacy purposes, to allow existing BlazeAndroidTestRunConfigurations to be updated
- *     to BlazeCommandRunConfigurations. Intended to be removed in 2.1.
- */
-// Hack: extend UnknownConfigurationType to completely hide it in the Run/Debug Configurations UI.
-@Deprecated
-public class BlazeAndroidTestRunConfigurationType extends UnknownConfigurationType {
-  private static final Icon ANDROID_TEST_ICON;
-
-  static {
-    LayeredIcon icon = new LayeredIcon(2);
-    icon.setIcon(AndroidIcons.Android, 0);
-    icon.setIcon(AllIcons.Nodes.JunitTestMark, 1);
-    ANDROID_TEST_ICON = icon;
-  }
-
-  private final BlazeAndroidTestRunConfigurationFactory factory =
-      new BlazeAndroidTestRunConfigurationFactory(this);
-
-  static class BlazeAndroidTestRunConfigurationFactory extends ConfigurationFactory {
-
-    protected BlazeAndroidTestRunConfigurationFactory(@NotNull ConfigurationType type) {
-      super(type);
-    }
-
-    @Override
-    public String getName() {
-      // Used to look up this ConfigurationFactory.
-      // Preserve value so legacy configurations can be loaded.
-      return Blaze.defaultBuildSystemName() + " Android Test";
-    }
-
-    @Override
-    @NotNull
-    public BlazeCommandRunConfiguration createTemplateConfiguration(@NotNull Project project) {
-      // Create a BlazeCommandRunConfiguration instead, to update legacy configurations.
-      return BlazeCommandRunConfigurationType.getInstance()
-          .getFactory()
-          .createTemplateConfiguration(project);
-    }
-
-    @Override
-    public boolean canConfigurationBeSingleton() {
-      return false;
-    }
-
-    @Override
-    public boolean isApplicable(@NotNull Project project) {
-      return Blaze.isBlazeProject(project);
-    }
-
-    @Override
-    public void configureBeforeRunTaskDefaults(
-        Key<? extends BeforeRunTask> providerID, BeforeRunTask task) {
-      // Removed BlazeAndroidBeforeRunTaskProvider; this method won't be called anymore anyhow.
-    }
-
-    @Override
-    public boolean isConfigurationSingletonByDefault() {
-      return true;
-    }
-  }
-
-  public static BlazeAndroidTestRunConfigurationType getInstance() {
-    return ConfigurationTypeUtil.findConfigurationType(BlazeAndroidTestRunConfigurationType.class);
-  }
-
-  @Override
-  public String getDisplayName() {
-    return "Legacy " + Blaze.defaultBuildSystemName() + " Android Test";
-  }
-
-  @Override
-  public String getConfigurationTypeDescription() {
-    return "Launch/debug configuration for android_test rules "
-        + "Use Blaze Command instead; this legacy configuration type is being removed.";
-  }
-
-  @Override
-  public Icon getIcon() {
-    return ANDROID_TEST_ICON;
-  }
-
-  @Override
-  @NotNull
-  public String getId() {
-    // Used to look up this ConfigurationType.
-    // Preserve value so legacy configurations can be loaded.
-    return "BlazeAndroidTestRunConfigurationType";
-  }
-
-  @Override
-  public BlazeAndroidTestRunConfigurationFactory[] getConfigurationFactories() {
-    return new BlazeAndroidTestRunConfigurationFactory[] {factory};
-  }
-}
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 ee1013b..cf418e9 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
@@ -33,6 +33,7 @@
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.Lists;
 import com.google.common.util.concurrent.Futures;
+import com.google.idea.blaze.android.compatibility.Compatibility;
 import com.google.idea.blaze.android.run.deployinfo.BlazeAndroidDeployInfo;
 import com.google.idea.blaze.android.run.deployinfo.BlazeApkProvider;
 import com.google.idea.blaze.android.run.runner.BlazeAndroidDeviceSelector;
@@ -41,9 +42,13 @@
 import com.google.idea.blaze.android.run.runner.BlazeAndroidRunContext;
 import com.google.idea.blaze.android.run.runner.BlazeApkBuildStep;
 import com.google.idea.blaze.android.run.runner.BlazeApkBuildStepNormalBuild;
+import com.google.idea.blaze.android.run.test.smrunner.BlazeAndroidTestEventsHandler;
 import com.google.idea.blaze.base.model.primitives.Label;
+import com.google.idea.common.experiments.BoolExperiment;
 import com.intellij.execution.ExecutionException;
+import com.intellij.execution.Executor;
 import com.intellij.execution.configurations.RunConfiguration;
+import com.intellij.execution.executors.DefaultDebugExecutor;
 import com.intellij.execution.runners.ExecutionEnvironment;
 import com.intellij.openapi.project.Project;
 import java.util.Collection;
@@ -55,6 +60,10 @@
 
 /** Run context for android_test. */
 class BlazeAndroidTestRunContext implements BlazeAndroidRunContext {
+
+  static final BoolExperiment smRunnerUiEnabled =
+      new BoolExperiment("use.smrunner.ui.android", true);
+
   private final Project project;
   private final AndroidFacet facet;
   private final RunConfiguration runConfiguration;
@@ -82,12 +91,28 @@
     this.env = env;
     this.label = label;
     this.configState = configState;
-    this.buildFlags = buildFlags;
-    this.consoleProvider = new AndroidTestConsoleProvider(project, runConfiguration, configState);
     this.buildStep = new BlazeApkBuildStepNormalBuild(project, label, buildFlags);
     this.applicationIdProvider =
         new BlazeAndroidTestApplicationIdProvider(project, buildStep.getDeployInfo());
     this.apkProvider = new BlazeApkProvider(project, buildStep.getDeployInfo());
+
+    BlazeAndroidTestEventsHandler testEventsHandler = null;
+    if (smRunnerUiEnabled.getValue() && !isDebugging(env.getExecutor())) {
+      testEventsHandler = new BlazeAndroidTestEventsHandler();
+      this.buildFlags =
+          ImmutableList.<String>builder()
+              .addAll(testEventsHandler.getBlazeFlags())
+              .addAll(buildFlags)
+              .build();
+    } else {
+      this.buildFlags = buildFlags;
+    }
+    this.consoleProvider =
+        new AndroidTestConsoleProvider(project, runConfiguration, configState, testEventsHandler);
+  }
+
+  private static boolean isDebugging(Executor executor) {
+    return executor instanceof DefaultDebugExecutor;
   }
 
   @Override
@@ -192,18 +217,26 @@
   }
 
   @Override
+  @SuppressWarnings("unchecked")
   public DebugConnectorTask getDebuggerTask(
       AndroidDebugger androidDebugger,
       AndroidDebuggerState androidDebuggerState,
-      @NotNull Set<String> packageIds)
+      @NotNull Set<String> packageIds,
+      boolean monitorRemoteProcess)
       throws ExecutionException {
     if (configState.isRunThroughBlaze()) {
       return new ConnectBlazeTestDebuggerTask(
           env.getProject(), androidDebugger, packageIds, applicationIdProvider, this);
     }
-    //noinspection unchecked
-    return androidDebugger.getConnectDebuggerTask(
-        env, null, packageIds, facet, androidDebuggerState, runConfiguration.getType().getId());
+    return Compatibility.getConnectDebuggerTask(
+        androidDebugger,
+        env,
+        null,
+        packageIds,
+        facet,
+        androidDebuggerState,
+        runConfiguration.getType().getId(),
+        monitorRemoteProcess);
   }
 
   void onLaunchTaskComplete() {
diff --git a/aswb/src/com/google/idea/blaze/android/run/test/ConnectBlazeTestDebuggerTask.java b/aswb/src/com/google/idea/blaze/android/run/test/ConnectBlazeTestDebuggerTask.java
index 7153880..8e2328e 100644
--- a/aswb/src/com/google/idea/blaze/android/run/test/ConnectBlazeTestDebuggerTask.java
+++ b/aswb/src/com/google/idea/blaze/android/run/test/ConnectBlazeTestDebuggerTask.java
@@ -28,9 +28,9 @@
 import com.android.tools.idea.run.LaunchInfo;
 import com.android.tools.idea.run.ProcessHandlerConsolePrinter;
 import com.android.tools.idea.run.editor.AndroidDebugger;
-import com.android.tools.idea.run.tasks.ConnectDebuggerTask;
 import com.android.tools.idea.run.tasks.ConnectJavaDebuggerTask;
 import com.android.tools.idea.run.util.ProcessHandlerLaunchStatus;
+import com.google.idea.blaze.android.compatibility.Compatibility.ConnectDebuggerTask;
 import com.intellij.debugger.engine.RemoteDebugProcessHandler;
 import com.intellij.debugger.ui.DebuggerPanelsManager;
 import com.intellij.execution.ExecutionException;
@@ -62,7 +62,7 @@
       Set<String> applicationIds,
       ApplicationIdProvider applicationIdProvider,
       BlazeAndroidTestRunContext runContext) {
-    super(applicationIds, debugger, project);
+    super(applicationIds, debugger, project, true);
     this.project = project;
     this.applicationIdProvider = applicationIdProvider;
     this.runContext = runContext;
diff --git a/aswb/src/com/google/idea/blaze/android/run/test/StockAndroidTestLaunchTask.java b/aswb/src/com/google/idea/blaze/android/run/test/StockAndroidTestLaunchTask.java
index cf3664b..2430f3c 100644
--- a/aswb/src/com/google/idea/blaze/android/run/test/StockAndroidTestLaunchTask.java
+++ b/aswb/src/com/google/idea/blaze/android/run/test/StockAndroidTestLaunchTask.java
@@ -21,9 +21,9 @@
 import com.android.tools.idea.run.ApplicationIdProvider;
 import com.android.tools.idea.run.ConsolePrinter;
 import com.android.tools.idea.run.tasks.LaunchTask;
-import com.android.tools.idea.run.testing.AndroidTestListener;
 import com.android.tools.idea.run.util.LaunchStatus;
 import com.google.common.collect.ImmutableList;
+import com.google.idea.blaze.android.compatibility.Compatibility.AndroidTestListener;
 import com.google.idea.blaze.android.run.deployinfo.BlazeAndroidDeployInfo;
 import com.intellij.openapi.application.ApplicationManager;
 import com.intellij.openapi.diagnostic.Logger;
diff --git a/aswb/src/com/google/idea/blaze/android/run/test/smrunner/BlazeAndroidTestEventsHandler.java b/aswb/src/com/google/idea/blaze/android/run/test/smrunner/BlazeAndroidTestEventsHandler.java
new file mode 100644
index 0000000..f37359a
--- /dev/null
+++ b/aswb/src/com/google/idea/blaze/android/run/test/smrunner/BlazeAndroidTestEventsHandler.java
@@ -0,0 +1,110 @@
+/*
+ * 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.run.test.smrunner;
+
+import com.google.idea.blaze.base.command.BlazeFlags;
+import com.google.idea.blaze.base.run.smrunner.BlazeTestEventsHandler;
+import com.google.idea.blaze.base.run.smrunner.SmRunnerUtils;
+import com.google.idea.blaze.java.run.producers.BlazeJUnitTestFilterFlags;
+import com.google.idea.blaze.java.run.producers.BlazeJUnitTestFilterFlags.JUnitVersion;
+import com.intellij.execution.Location;
+import com.intellij.execution.testframework.AbstractTestProxy;
+import com.intellij.execution.testframework.sm.runner.SMTestLocator;
+import com.intellij.openapi.project.Project;
+import com.intellij.openapi.util.text.StringUtil;
+import com.intellij.psi.PsiClass;
+import com.intellij.psi.PsiElement;
+import com.intellij.psi.PsiMethod;
+import com.intellij.psi.search.GlobalSearchScope;
+import com.intellij.util.containers.MultiMap;
+import com.intellij.util.io.URLUtil;
+import java.util.List;
+import javax.annotation.Nullable;
+
+/** Provides java-specific methods needed by the SM-runner test UI. */
+public class BlazeAndroidTestEventsHandler extends BlazeTestEventsHandler {
+
+  public BlazeAndroidTestEventsHandler() {
+    super("Blaze Android Test");
+  }
+
+  @Override
+  public SMTestLocator getTestLocator() {
+    return BlazeAndroidTestLocator.INSTANCE;
+  }
+
+  @Override
+  public String suiteLocationUrl(String name) {
+    return SmRunnerUtils.GENERIC_SUITE_PROTOCOL + URLUtil.SCHEME_SEPARATOR + name;
+  }
+
+  @Override
+  public String testLocationUrl(String name, @Nullable String className) {
+    // ignore initial value of className -- it's the test runner class.
+    name = StringUtil.trimTrailing(name, '-');
+    if (!name.contains("-")) {
+      return SmRunnerUtils.GENERIC_SUITE_PROTOCOL + URLUtil.SCHEME_SEPARATOR + name;
+    }
+    int ix = name.lastIndexOf('-');
+    className = name.substring(0, ix);
+    String methodName = name.substring(ix + 1);
+    return SmRunnerUtils.GENERIC_SUITE_PROTOCOL
+        + URLUtil.SCHEME_SEPARATOR
+        + className
+        + SmRunnerUtils.TEST_NAME_PARTS_SPLITTER
+        + methodName;
+  }
+
+  @Override
+  public String testDisplayName(String rawName) {
+    String name = StringUtil.trimTrailing(rawName, '-');
+    if (name.contains("-")) {
+      int ix = name.lastIndexOf('-');
+      return name.substring(ix + 1);
+    }
+    return name;
+  }
+
+  @Nullable
+  @Override
+  public String getTestFilter(Project project, List<AbstractTestProxy> failedTests) {
+    GlobalSearchScope projectScope = GlobalSearchScope.allScope(project);
+    MultiMap<PsiClass, PsiMethod> failedMethodsPerClass = new MultiMap<>();
+    for (AbstractTestProxy test : failedTests) {
+      appendTest(failedMethodsPerClass, test.getLocation(project, projectScope));
+    }
+    // the android test runner always runs with JUnit4
+    String filter =
+        BlazeJUnitTestFilterFlags.testFilterForClassesAndMethods(
+            failedMethodsPerClass, JUnitVersion.JUNIT_4);
+    return filter != null ? BlazeFlags.TEST_FILTER + "=" + filter : null;
+  }
+
+  private void appendTest(
+      MultiMap<PsiClass, PsiMethod> testMap, @Nullable Location<?> testLocation) {
+    if (testLocation == null) {
+      return;
+    }
+    PsiElement method = testLocation.getPsiElement();
+    if (!(method instanceof PsiMethod)) {
+      return;
+    }
+    PsiClass psiClass = ((PsiMethod) method).getContainingClass();
+    if (psiClass != null) {
+      testMap.putValue(psiClass, (PsiMethod) method);
+    }
+  }
+}
diff --git a/aswb/src/com/google/idea/blaze/android/run/test/smrunner/BlazeAndroidTestLocator.java b/aswb/src/com/google/idea/blaze/android/run/test/smrunner/BlazeAndroidTestLocator.java
new file mode 100644
index 0000000..c03f759
--- /dev/null
+++ b/aswb/src/com/google/idea/blaze/android/run/test/smrunner/BlazeAndroidTestLocator.java
@@ -0,0 +1,96 @@
+/*
+ * 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.run.test.smrunner;
+
+import com.google.common.collect.ImmutableList;
+import com.google.idea.blaze.base.run.smrunner.SmRunnerUtils;
+import com.intellij.execution.Location;
+import com.intellij.execution.PsiLocation;
+import com.intellij.execution.junit.JUnitUtil;
+import com.intellij.execution.testframework.sm.runner.SMTestLocator;
+import com.intellij.openapi.project.Project;
+import com.intellij.psi.JavaPsiFacade;
+import com.intellij.psi.PsiClass;
+import com.intellij.psi.PsiMethod;
+import com.intellij.psi.search.GlobalSearchScope;
+import com.intellij.psi.search.PsiShortNamesCache;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.stream.Collectors;
+
+/** Locate android test classes / methods for test UI navigation. */
+public class BlazeAndroidTestLocator implements SMTestLocator {
+
+  public static final BlazeAndroidTestLocator INSTANCE = new BlazeAndroidTestLocator();
+
+  private BlazeAndroidTestLocator() {}
+
+  @Override
+  public List<Location> getLocation(
+      String protocol, String path, Project project, GlobalSearchScope scope) {
+    if (!protocol.equals(SmRunnerUtils.GENERIC_SUITE_PROTOCOL)) {
+      return ImmutableList.of();
+    }
+    String[] split = path.split(SmRunnerUtils.TEST_NAME_PARTS_SPLITTER);
+    List<PsiClass> classes = findClasses(project, scope, split[0]);
+    if (classes.isEmpty()) {
+      return ImmutableList.of();
+    }
+    if (split.length <= 1) {
+      return classes.stream().map(PsiLocation::new).collect(Collectors.toList());
+    }
+
+    String methodName = split[1];
+    List<Location> results = new ArrayList<>();
+    for (PsiClass psiClass : classes) {
+      PsiMethod method = findTestMethod(psiClass, methodName);
+      if (method != null) {
+        results.add(new PsiLocation<>(method));
+      }
+    }
+    return results.isEmpty() ? ImmutableList.of(new PsiLocation<>(classes.get(0))) : results;
+  }
+
+  private static List<PsiClass> findClasses(
+      Project project, GlobalSearchScope scope, String className) {
+    PsiClass psiClass = JavaPsiFacade.getInstance(project).findClass(className, scope);
+    if (psiClass != null) {
+      return ImmutableList.of(psiClass);
+    }
+    // handle unqualified class names
+    return Arrays.stream(PsiShortNamesCache.getInstance(project).getClassesByName(className, scope))
+        .filter(JUnitUtil::isTestClass)
+        .collect(Collectors.toList());
+  }
+
+  private static PsiMethod findTestMethod(PsiClass psiClass, String methodName) {
+    final PsiMethod[] methods = psiClass.findMethodsByName(methodName, true);
+
+    if (methods.length == 0) {
+      return null;
+    }
+    if (methods.length == 1) {
+      return methods[0];
+    }
+    for (PsiMethod method : methods) {
+      if (method.getParameterList().getParametersCount() == 0) {
+        return method;
+      }
+    }
+    return methods[0];
+  }
+}
diff --git a/aswb/src/com/google/idea/blaze/android/settings/BlazeAndroidUserSettings.java b/aswb/src/com/google/idea/blaze/android/settings/BlazeAndroidUserSettings.java
new file mode 100644
index 0000000..6d9487b
--- /dev/null
+++ b/aswb/src/com/google/idea/blaze/android/settings/BlazeAndroidUserSettings.java
@@ -0,0 +1,54 @@
+/*
+ * 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.settings;
+
+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.util.xmlb.XmlSerializerUtil;
+import org.jetbrains.annotations.NotNull;
+
+/** Android-specific user settings. */
+@State(name = "BlazeAndroidUserSettings", storages = @Storage("blaze.android.user.settings.xml"))
+public class BlazeAndroidUserSettings
+    implements PersistentStateComponent<BlazeAndroidUserSettings> {
+
+  private boolean useLayoutEditor = false;
+
+  public static BlazeAndroidUserSettings getInstance() {
+    return ServiceManager.getService(BlazeAndroidUserSettings.class);
+  }
+
+  @Override
+  @NotNull
+  public BlazeAndroidUserSettings getState() {
+    return this;
+  }
+
+  @Override
+  public void loadState(BlazeAndroidUserSettings state) {
+    XmlSerializerUtil.copyBean(state, this);
+  }
+
+  public boolean getUseLayoutEditor() {
+    return useLayoutEditor;
+  }
+
+  public void setUseLayoutEditor(boolean useLayoutEditor) {
+    this.useLayoutEditor = useLayoutEditor;
+  }
+}
diff --git a/aswb/src/com/google/idea/blaze/android/sync/BazelAndroidJdkProvider.java b/aswb/src/com/google/idea/blaze/android/sync/BazelAndroidJdkProvider.java
index f45b368..d603ff3 100644
--- a/aswb/src/com/google/idea/blaze/android/sync/BazelAndroidJdkProvider.java
+++ b/aswb/src/com/google/idea/blaze/android/sync/BazelAndroidJdkProvider.java
@@ -15,9 +15,8 @@
  */
 package com.google.idea.blaze.android.sync;
 
-import com.google.idea.blaze.base.settings.Blaze;
+import com.google.idea.blaze.base.model.BlazeVersionData;
 import com.google.idea.blaze.base.settings.Blaze.BuildSystem;
-import com.intellij.openapi.project.Project;
 import com.intellij.pom.java.LanguageLevel;
 import javax.annotation.Nullable;
 
@@ -26,8 +25,11 @@
 
   @Nullable
   @Override
-  public LanguageLevel getLanguageLevel(Project project) {
-    BuildSystem buildSystem = Blaze.getBuildSystem(project);
-    return buildSystem == BuildSystem.Bazel ? LanguageLevel.JDK_1_7 : null;
+  public LanguageLevel getLanguageLevel(
+      BuildSystem buildSystem, BlazeVersionData blazeVersionData) {
+    if (buildSystem != BuildSystem.Bazel) {
+      return null;
+    }
+    return LanguageLevel.JDK_1_7;
   }
 }
diff --git a/aswb/src/com/google/idea/blaze/android/sync/BlazeAndroidSyncListener.java b/aswb/src/com/google/idea/blaze/android/sync/BlazeAndroidSyncListener.java
index 815c4c9..253b4db 100644
--- a/aswb/src/com/google/idea/blaze/android/sync/BlazeAndroidSyncListener.java
+++ b/aswb/src/com/google/idea/blaze/android/sync/BlazeAndroidSyncListener.java
@@ -27,7 +27,7 @@
   @Override
   public void afterSync(
       Project project, BlazeContext context, SyncMode syncMode, SyncResult syncResult) {
-    if (syncResult == SyncResult.SUCCESS || syncResult == SyncResult.PARTIAL_SUCCESS) {
+    if (syncResult.successful()) {
       DumbService dumbService = DumbService.getInstance(project);
       dumbService.queueTask(new ResourceFolderRegistry.PopulateCachesTask(project));
     }
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 72b352d..74d41e7 100644
--- a/aswb/src/com/google/idea/blaze/android/sync/BlazeAndroidSyncPlugin.java
+++ b/aswb/src/com/google/idea/blaze/android/sync/BlazeAndroidSyncPlugin.java
@@ -15,9 +15,10 @@
  */
 package com.google.idea.blaze.android.sync;
 
-import com.android.tools.idea.sdk.IdeSdks;
 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.AndroidSdkPlatformSection;
 import com.google.idea.blaze.android.projectview.GeneratedAndroidResourcesSection;
@@ -29,6 +30,7 @@
 import com.google.idea.blaze.android.sync.sdk.AndroidSdkFromProjectView;
 import com.google.idea.blaze.base.ideinfo.TargetMap;
 import com.google.idea.blaze.base.model.BlazeProjectData;
+import com.google.idea.blaze.base.model.BlazeVersionData;
 import com.google.idea.blaze.base.model.SyncState;
 import com.google.idea.blaze.base.model.primitives.LanguageClass;
 import com.google.idea.blaze.base.model.primitives.WorkspaceRoot;
@@ -40,6 +42,7 @@
 import com.google.idea.blaze.base.scope.output.IssueOutput;
 import com.google.idea.blaze.base.scope.output.StatusOutput;
 import com.google.idea.blaze.base.scope.scopes.TimingScope;
+import com.google.idea.blaze.base.settings.Blaze;
 import com.google.idea.blaze.base.sync.BlazeSyncPlugin;
 import com.google.idea.blaze.base.sync.SourceFolderProvider;
 import com.google.idea.blaze.base.sync.libraries.LibrarySource;
@@ -69,7 +72,6 @@
 import java.util.Set;
 import javax.annotation.Nullable;
 import org.jetbrains.android.facet.AndroidFacet;
-import org.jetbrains.android.sdk.AndroidSdkUtils;
 
 /** ASwB sync plugin. */
 public class BlazeAndroidSyncPlugin extends BlazeSyncPlugin.Adapter {
@@ -160,6 +162,7 @@
       Project project,
       BlazeContext context,
       ProjectViewSet projectViewSet,
+      BlazeVersionData blazeVersionData,
       BlazeProjectData blazeProjectData) {
     if (!isAndroidWorkspace(blazeProjectData.workspaceLanguageSettings)) {
       return;
@@ -180,7 +183,9 @@
       return;
     }
 
-    LanguageLevel defaultLanguageLevel = BuildSystemAndroidJdkProvider.languageLevel(project);
+    LanguageLevel defaultLanguageLevel =
+        BuildSystemAndroidJdkProvider.languageLevel(
+            Blaze.getBuildSystem(project), blazeVersionData);
     LanguageLevel javaLanguageLevel =
         JavaLanguageLevelSection.getLanguageLevel(projectViewSet, defaultLanguageLevel);
     setProjectSdkAndLanguageLevel(project, sdk, javaLanguageLevel);
@@ -200,7 +205,6 @@
     BlazeAndroidProjectStructureSyncer.updateProjectStructure(
         project,
         context,
-        workspaceRoot,
         projectViewSet,
         blazeProjectData,
         moduleEditor,
@@ -209,6 +213,23 @@
         isAndroidWorkspace(blazeProjectData.workspaceLanguageSettings));
   }
 
+  @Override
+  public void updateInMemoryState(
+      Project project,
+      BlazeContext context,
+      WorkspaceRoot workspaceRoot,
+      ProjectViewSet projectViewSet,
+      BlazeProjectData blazeProjectData,
+      Module workspaceModule) {
+    BlazeAndroidProjectStructureSyncer.updateInMemoryState(
+        project,
+        workspaceRoot,
+        projectViewSet,
+        blazeProjectData,
+        workspaceModule,
+        isAndroidWorkspace(blazeProjectData.workspaceLanguageSettings));
+  }
+
   @Nullable
   @Override
   public SourceFolderProvider getSourceFolderProvider(BlazeProjectData projectData) {
diff --git a/aswb/src/com/google/idea/blaze/android/sync/BuildSystemAndroidJdkProvider.java b/aswb/src/com/google/idea/blaze/android/sync/BuildSystemAndroidJdkProvider.java
index d9b53bf..94c4c9a 100644
--- a/aswb/src/com/google/idea/blaze/android/sync/BuildSystemAndroidJdkProvider.java
+++ b/aswb/src/com/google/idea/blaze/android/sync/BuildSystemAndroidJdkProvider.java
@@ -15,8 +15,9 @@
  */
 package com.google.idea.blaze.android.sync;
 
+import com.google.idea.blaze.base.model.BlazeVersionData;
+import com.google.idea.blaze.base.settings.Blaze.BuildSystem;
 import com.intellij.openapi.extensions.ExtensionPointName;
-import com.intellij.openapi.project.Project;
 import com.intellij.pom.java.LanguageLevel;
 import javax.annotation.Nullable;
 
@@ -28,9 +29,9 @@
 
   LanguageLevel DEFAULT_LANGUAGE_LEVEL = LanguageLevel.JDK_1_7;
 
-  static LanguageLevel languageLevel(Project project) {
+  static LanguageLevel languageLevel(BuildSystem buildSystem, BlazeVersionData blazeVersionData) {
     for (BuildSystemAndroidJdkProvider provider : EP_NAME.getExtensions()) {
-      LanguageLevel level = provider.getLanguageLevel(project);
+      LanguageLevel level = provider.getLanguageLevel(buildSystem, blazeVersionData);
       if (level != null) {
         return level;
       }
@@ -43,5 +44,5 @@
    * unable to determine it.
    */
   @Nullable
-  LanguageLevel getLanguageLevel(Project project);
+  LanguageLevel getLanguageLevel(BuildSystem buildSystem, BlazeVersionData blazeVersionData);
 }
diff --git a/aswb/src/com/google/idea/blaze/android/sync/model/AndroidResourceModuleRegistry.java b/aswb/src/com/google/idea/blaze/android/sync/model/AndroidResourceModuleRegistry.java
new file mode 100644
index 0000000..a1f6bf8
--- /dev/null
+++ b/aswb/src/com/google/idea/blaze/android/sync/model/AndroidResourceModuleRegistry.java
@@ -0,0 +1,55 @@
+/*
+ * 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.sync.model;
+
+import com.google.common.collect.BiMap;
+import com.google.common.collect.HashBiMap;
+import com.google.idea.blaze.base.ideinfo.TargetKey;
+import com.google.idea.blaze.base.model.primitives.Label;
+import com.intellij.openapi.components.ServiceManager;
+import com.intellij.openapi.module.Module;
+import com.intellij.openapi.project.Project;
+
+/** Keeps track of which resource modules correspond to which resource target. */
+public class AndroidResourceModuleRegistry {
+  private final BiMap<Module, AndroidResourceModule> moduleMap = HashBiMap.create();
+
+  public static AndroidResourceModuleRegistry getInstance(Project project) {
+    return ServiceManager.getService(project, AndroidResourceModuleRegistry.class);
+  }
+
+  public Label getLabel(Module module) {
+    TargetKey targetKey = getTargetKey(module);
+    return targetKey == null ? null : targetKey.label;
+  }
+
+  public TargetKey getTargetKey(Module module) {
+    AndroidResourceModule resourceModule = get(module);
+    return resourceModule == null ? null : resourceModule.targetKey;
+  }
+
+  public AndroidResourceModule get(Module module) {
+    return moduleMap.get(module);
+  }
+
+  public void put(Module module, AndroidResourceModule resourceModule) {
+    moduleMap.put(module, resourceModule);
+  }
+
+  public void clear() {
+    moduleMap.clear();
+  }
+}
diff --git a/aswb/src/com/google/idea/blaze/android/sync/model/idea/BlazeAndroidModel.java b/aswb/src/com/google/idea/blaze/android/sync/model/idea/BlazeAndroidModel.java
index 7a13e1e..8e19be7 100644
--- a/aswb/src/com/google/idea/blaze/android/sync/model/idea/BlazeAndroidModel.java
+++ b/aswb/src/com/google/idea/blaze/android/sync/model/idea/BlazeAndroidModel.java
@@ -22,16 +22,16 @@
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.Sets;
 import com.google.idea.blaze.android.manifest.ManifestParser;
-import com.intellij.openapi.diagnostic.Logger;
+import com.intellij.openapi.application.ApplicationManager;
 import com.intellij.openapi.module.Module;
 import com.intellij.openapi.project.Project;
+import com.intellij.openapi.util.Computable;
 import com.intellij.openapi.vfs.LocalFileSystem;
 import com.intellij.openapi.vfs.VirtualFile;
 import java.io.File;
 import java.util.List;
 import java.util.Set;
 import org.jetbrains.android.dom.manifest.Manifest;
-import org.jetbrains.annotations.NotNull;
 import org.jetbrains.annotations.Nullable;
 
 /**
@@ -39,10 +39,7 @@
  * user-selected build variant.
  */
 public class BlazeAndroidModel implements AndroidModel {
-  private static final Logger LOG = Logger.getInstance(BlazeAndroidModel.class);
-
   private Project project;
-  private Module module;
   private final File rootDirPath;
   private final SourceProvider sourceProvider;
   private final List<SourceProvider> sourceProviders; // Singleton list of sourceProvider
@@ -60,7 +57,6 @@
       String resourceJavaPackage,
       int androidSdkApiLevel) {
     this.project = project;
-    this.module = module;
     this.rootDirPath = rootDirPath;
     this.sourceProvider = sourceProvider;
     this.sourceProviders = ImmutableList.of(sourceProvider);
@@ -69,45 +65,43 @@
     this.androidSdkApiLevel = androidSdkApiLevel;
   }
 
-  @NotNull
   @Override
   public SourceProvider getDefaultSourceProvider() {
     return sourceProvider;
   }
 
-  @NotNull
   @Override
   public List<SourceProvider> getActiveSourceProviders() {
     return sourceProviders;
   }
 
-  @NotNull
   @Override
   public List<SourceProvider> getTestSourceProviders() {
     return sourceProviders;
   }
 
-  @NotNull
   @Override
   public List<SourceProvider> getAllSourceProviders() {
     return sourceProviders;
   }
 
   @Override
-  @NotNull
   public String getApplicationId() {
-    String result = null;
-    Manifest manifest = ManifestParser.getInstance(project).getManifest(moduleManifest);
-    if (manifest != null) {
-      result = manifest.getPackage().getValue();
-    }
-    if (result == null) {
-      result = resourceJavaPackage;
-    }
-    return result;
+    // Run in a read action since otherwise, it might throw a read access exception.
+    return ApplicationManager.getApplication()
+        .runReadAction(
+            (Computable<String>)
+                () -> {
+                  Manifest manifest =
+                      ManifestParser.getInstance(project).getManifest(moduleManifest);
+                  if (manifest == null) {
+                    return resourceJavaPackage;
+                  }
+                  String packageName = manifest.getPackage().getValue();
+                  return packageName == null ? resourceJavaPackage : packageName;
+                });
   }
 
-  @NotNull
   @Override
   public Set<String> getAllApplicationIds() {
     Set<String> applicationIds = Sets.newHashSet();
@@ -149,18 +143,16 @@
     return null;
   }
 
-  @NotNull
   @Override
   public File getRootDirPath() {
     return rootDirPath;
   }
 
   @Override
-  public boolean isGenerated(@NotNull VirtualFile file) {
+  public boolean isGenerated(VirtualFile file) {
     return false;
   }
 
-  @NotNull
   @Override
   public VirtualFile getRootDir() {
     File rootDirPath = getRootDirPath();
@@ -175,14 +167,13 @@
   }
 
   @Override
-  @NotNull
   public ClassJarProvider getClassJarProvider() {
-    return new NullClassJarProvider();
+    return new BlazeClassJarProvider(project);
   }
 
   @Override
   @Nullable
-  public Long getLastBuildTimestamp(@NotNull Project project) {
+  public Long getLastBuildTimestamp(Project project) {
     // TODO(jvoung): Coordinate with blaze build actions to be able determine last build time.
     return null;
   }
diff --git a/aswb/src/com/google/idea/blaze/android/sync/model/idea/BlazeClassJarProvider.java b/aswb/src/com/google/idea/blaze/android/sync/model/idea/BlazeClassJarProvider.java
deleted file mode 100644
index dbd24d6..0000000
--- a/aswb/src/com/google/idea/blaze/android/sync/model/idea/BlazeClassJarProvider.java
+++ /dev/null
@@ -1,112 +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.sync.model.idea;
-
-import com.android.SdkConstants;
-import com.android.tools.idea.model.ClassJarProvider;
-import com.google.idea.blaze.base.ideinfo.ArtifactLocation;
-import com.google.idea.blaze.base.ideinfo.LibraryArtifact;
-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.google.idea.blaze.java.sync.model.BlazeJarLibrary;
-import com.google.idea.blaze.java.sync.model.BlazeJavaSyncData;
-import com.intellij.openapi.module.Module;
-import com.intellij.openapi.project.Project;
-import com.intellij.openapi.vfs.JarFileSystem;
-import com.intellij.openapi.vfs.LocalFileSystem;
-import com.intellij.openapi.vfs.VirtualFile;
-import com.intellij.util.containers.OrderedSet;
-import java.io.File;
-import java.util.List;
-import org.jetbrains.annotations.Nullable;
-
-/** Collects class jars from the user's build. */
-public class BlazeClassJarProvider extends ClassJarProvider {
-
-  private final Project project;
-
-  public BlazeClassJarProvider(final Project project) {
-    this.project = project;
-  }
-
-  @Override
-  @Nullable
-  public VirtualFile findModuleClassFile(String className, Module module) {
-    BlazeProjectData blazeProjectData =
-        BlazeProjectDataManager.getInstance(project).getBlazeProjectData();
-    if (blazeProjectData == null) {
-      return null;
-    }
-    LocalFileSystem localVfs = LocalFileSystem.getInstance();
-    String classNamePath = className.replace('.', File.separatorChar) + SdkConstants.DOT_CLASS;
-    BlazeJavaSyncData syncData = blazeProjectData.syncState.get(BlazeJavaSyncData.class);
-    if (syncData == null) {
-      return null;
-    }
-    ArtifactLocationDecoder artifactLocationDecoder = blazeProjectData.artifactLocationDecoder;
-    for (File classJar : artifactLocationDecoder.decodeAll(syncData.importResult.buildOutputJars)) {
-      VirtualFile classJarVF = localVfs.findFileByIoFile(classJar);
-      if (classJarVF == null) {
-        continue;
-      }
-      VirtualFile classFile = findClassInJar(classJarVF, classNamePath);
-      if (classFile != null) {
-        return classFile;
-      }
-    }
-    return null;
-  }
-
-  @Nullable
-  private static VirtualFile findClassInJar(final VirtualFile classJar, String classNamePath) {
-    VirtualFile jarRoot = JarFileSystem.getInstance().getJarRootForLocalFile(classJar);
-    if (jarRoot == null) {
-      return null;
-    }
-    return jarRoot.findFileByRelativePath(classNamePath);
-  }
-
-  @Override
-  public List<VirtualFile> getModuleExternalLibraries(Module module) {
-    OrderedSet<VirtualFile> results = new OrderedSet<VirtualFile>();
-    BlazeProjectData blazeProjectData =
-        BlazeProjectDataManager.getInstance(project).getBlazeProjectData();
-    if (blazeProjectData == null) {
-      return results;
-    }
-    BlazeJavaSyncData syncData = blazeProjectData.syncState.get(BlazeJavaSyncData.class);
-    if (syncData == null) {
-      return null;
-    }
-    ArtifactLocationDecoder artifactLocationDecoder = blazeProjectData.artifactLocationDecoder;
-    LocalFileSystem localVfs = LocalFileSystem.getInstance();
-    for (BlazeJarLibrary blazeLibrary : syncData.importResult.libraries.values()) {
-      LibraryArtifact libraryArtifact = blazeLibrary.libraryArtifact;
-      ArtifactLocation classJar = libraryArtifact.classJar;
-      if (classJar == null) {
-        continue;
-      }
-      VirtualFile libVF = localVfs.findFileByIoFile(artifactLocationDecoder.decode(classJar));
-      if (libVF == null) {
-        continue;
-      }
-      results.add(libVF);
-    }
-
-    return results;
-  }
-}
diff --git a/aswb/src/com/google/idea/blaze/android/sync/projectstructure/AndroidFacetModuleCustomizer.java b/aswb/src/com/google/idea/blaze/android/sync/projectstructure/AndroidFacetModuleCustomizer.java
index db6443a..541be52 100755
--- a/aswb/src/com/google/idea/blaze/android/sync/projectstructure/AndroidFacetModuleCustomizer.java
+++ b/aswb/src/com/google/idea/blaze/android/sync/projectstructure/AndroidFacetModuleCustomizer.java
@@ -16,6 +16,7 @@
 package com.google.idea.blaze.android.sync.projectstructure;
 
 import com.android.builder.model.AndroidProject;
+import com.google.idea.blaze.android.compatibility.Compatibility;
 import com.intellij.facet.FacetManager;
 import com.intellij.facet.ModifiableFacetModel;
 import com.intellij.openapi.module.Module;
@@ -55,7 +56,7 @@
   private static void configureFacet(AndroidFacet facet) {
     JpsAndroidModuleProperties facetState = facet.getProperties();
     facetState.ALLOW_USER_CONFIGURATION = false;
-    facetState.LIBRARY_PROJECT = true;
+    Compatibility.setFacetStateIsLibraryProject(facetState);
     facetState.MANIFEST_FILE_RELATIVE_PATH = "";
     facetState.RES_FOLDER_RELATIVE_PATH = "";
     facetState.ASSETS_FOLDER_RELATIVE_PATH = "";
diff --git a/aswb/src/com/google/idea/blaze/android/sync/projectstructure/BlazeAndroidProjectStructureSyncer.java b/aswb/src/com/google/idea/blaze/android/sync/projectstructure/BlazeAndroidProjectStructureSyncer.java
index 20d5462..cd5ff8f 100644
--- a/aswb/src/com/google/idea/blaze/android/sync/projectstructure/BlazeAndroidProjectStructureSyncer.java
+++ b/aswb/src/com/google/idea/blaze/android/sync/projectstructure/BlazeAndroidProjectStructureSyncer.java
@@ -15,14 +15,18 @@
  */
 package com.google.idea.blaze.android.sync.projectstructure;
 
+import static java.util.stream.Collectors.toSet;
+
 import com.android.builder.model.SourceProvider;
 import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Lists;
 import com.google.common.collect.Maps;
 import com.google.common.collect.Sets;
 import com.google.idea.blaze.android.projectview.GeneratedAndroidResourcesSection;
 import com.google.idea.blaze.android.resources.LightResourceClassService;
 import com.google.idea.blaze.android.run.BlazeAndroidRunConfigurationHandler;
 import com.google.idea.blaze.android.sync.model.AndroidResourceModule;
+import com.google.idea.blaze.android.sync.model.AndroidResourceModuleRegistry;
 import com.google.idea.blaze.android.sync.model.AndroidSdkPlatform;
 import com.google.idea.blaze.android.sync.model.BlazeAndroidSyncData;
 import com.google.idea.blaze.android.sync.model.idea.BlazeAndroidModel;
@@ -54,6 +58,7 @@
 import com.intellij.openapi.roots.ModifiableRootModel;
 import java.io.File;
 import java.util.Collection;
+import java.util.List;
 import java.util.Map;
 import java.util.Set;
 import javax.annotation.Nullable;
@@ -65,149 +70,141 @@
   public static void updateProjectStructure(
       Project project,
       BlazeContext context,
-      WorkspaceRoot workspaceRoot,
       ProjectViewSet projectViewSet,
       BlazeProjectData blazeProjectData,
       BlazeSyncPlugin.ModuleEditor moduleEditor,
       Module workspaceModule,
       ModifiableRootModel workspaceModifiableModel,
       boolean isAndroidWorkspace) {
-    LightResourceClassService.Builder rClassBuilder =
-        new LightResourceClassService.Builder(project);
-
-    if (isAndroidWorkspace) {
-      BlazeAndroidSyncData syncData = blazeProjectData.syncState.get(BlazeAndroidSyncData.class);
-      if (syncData == null) {
-        return;
-      }
-
-      AndroidSdkPlatform androidSdkPlatform = syncData.androidSdkPlatform;
-      if (androidSdkPlatform != null) {
-        int totalOrderEntries = 0;
-
-        // Create the workspace module
-        updateWorkspaceModule(project, workspaceRoot, workspaceModule, androidSdkPlatform);
-
-        // Create android resource modules
-        // Because we're setting up dependencies, the modules have to exist before we configure them
-        Map<TargetKey, AndroidResourceModule> targetToAndroidResourceModule = Maps.newHashMap();
-        for (AndroidResourceModule androidResourceModule :
-            syncData.importResult.androidResourceModules) {
-          targetToAndroidResourceModule.put(androidResourceModule.targetKey, androidResourceModule);
-          String moduleName = moduleNameForAndroidModule(androidResourceModule.targetKey);
-          moduleEditor.createModule(moduleName, StdModuleTypes.JAVA);
-        }
-
-        // Configure android resource modules
-        for (AndroidResourceModule androidResourceModule : targetToAndroidResourceModule.values()) {
-          TargetIdeInfo target = blazeProjectData.targetMap.get(androidResourceModule.targetKey);
-          AndroidIdeInfo androidIdeInfo = target.androidIdeInfo;
-          assert androidIdeInfo != null;
-
-          String moduleName = moduleNameForAndroidModule(target.key);
-          Module module = moduleEditor.findModule(moduleName);
-          assert module != null;
-          ModifiableRootModel modifiableRootModel = moduleEditor.editModule(module);
-
-          updateAndroidTargetModule(
-              project,
-              workspaceRoot,
-              blazeProjectData.artifactLocationDecoder,
-              androidSdkPlatform,
-              target,
-              module,
-              modifiableRootModel,
-              androidResourceModule);
-
-          for (TargetKey resourceDependency :
-              androidResourceModule.transitiveResourceDependencies) {
-            if (!targetToAndroidResourceModule.containsKey(resourceDependency)) {
-              continue;
-            }
-            String dependencyModuleName = moduleNameForAndroidModule(resourceDependency);
-            Module dependency = moduleEditor.findModule(dependencyModuleName);
-            if (dependency == null) {
-              continue;
-            }
-            modifiableRootModel.addModuleOrderEntry(dependency);
-            ++totalOrderEntries;
-          }
-          rClassBuilder.addRClass(androidIdeInfo.resourceJavaPackage, module);
-          // Add a dependency from the workspace to the resource module
-          workspaceModifiableModel.addModuleOrderEntry(module);
-        }
-
-        // Collect potential android run configuration targets
-        Set<Label> runConfigurationModuleTargets = Sets.newHashSet();
-
-        // Get all explicitly mentioned targets
-        // Doing this now will cut down on root changes later
-        for (TargetExpression targetExpression : projectViewSet.listItems(TargetSection.KEY)) {
-          if (!(targetExpression instanceof Label)) {
-            continue;
-          }
-          Label label = (Label) targetExpression;
-          runConfigurationModuleTargets.add(label);
-        }
-        // Get any pre-existing targets
-        for (RunConfiguration runConfiguration :
-            RunManager.getInstance(project).getAllConfigurationsList()) {
-          BlazeAndroidRunConfigurationHandler handler =
-              BlazeAndroidRunConfigurationHandler.getHandlerFrom(runConfiguration);
-          if (handler == null) {
-            continue;
-          }
-          runConfigurationModuleTargets.add(handler.getLabel());
-        }
-
-        int totalRunConfigurationModules = 0;
-        for (Label label : runConfigurationModuleTargets) {
-          TargetKey targetKey = TargetKey.forPlainTarget(label);
-          // If it's a resource module, it will already have been created
-          if (targetToAndroidResourceModule.containsKey(targetKey)) {
-            continue;
-          }
-          // Ensure the label is a supported android rule that exists
-          TargetIdeInfo target = blazeProjectData.targetMap.get(targetKey);
-          if (target == null) {
-            continue;
-          }
-          if (!target.kindIsOneOf(Kind.ANDROID_BINARY, Kind.ANDROID_TEST)) {
-            continue;
-          }
-
-          String moduleName = moduleNameForAndroidModule(targetKey);
-          Module module = moduleEditor.createModule(moduleName, StdModuleTypes.JAVA);
-          ModifiableRootModel modifiableRootModel = moduleEditor.editModule(module);
-          updateAndroidTargetModule(
-              project,
-              workspaceRoot,
-              blazeProjectData.artifactLocationDecoder,
-              androidSdkPlatform,
-              target,
-              module,
-              modifiableRootModel,
-              null);
-          ++totalRunConfigurationModules;
-        }
-
-        int whitelistedGenResources =
-            projectViewSet.listItems(GeneratedAndroidResourcesSection.KEY).size();
-        context.output(
-            PrintOutput.log(
-                String.format(
-                    "Android resource module count: %d, run config modules: %d, order entries: %d, "
-                        + "generated resources: %d",
-                    syncData.importResult.androidResourceModules.size(),
-                    totalRunConfigurationModules,
-                    totalOrderEntries,
-                    whitelistedGenResources)));
-      }
-    } else {
+    if (!isAndroidWorkspace) {
       AndroidFacetModuleCustomizer.removeAndroidFacet(workspaceModule);
+      return;
     }
 
-    LightResourceClassService.getInstance(project).installRClasses(rClassBuilder);
+    BlazeAndroidSyncData syncData = blazeProjectData.syncState.get(BlazeAndroidSyncData.class);
+    if (syncData == null) {
+      return;
+    }
+    AndroidSdkPlatform androidSdkPlatform = syncData.androidSdkPlatform;
+    if (androidSdkPlatform == null) {
+      return;
+    }
+
+    // Configure workspace module as an android module
+    AndroidFacetModuleCustomizer.createAndroidFacet(workspaceModule);
+
+    // Create android resource modules
+    // Because we're setting up dependencies, the modules have to exist before we configure them
+    Map<TargetKey, AndroidResourceModule> targetToAndroidResourceModule = Maps.newHashMap();
+    for (AndroidResourceModule androidResourceModule :
+        syncData.importResult.androidResourceModules) {
+      targetToAndroidResourceModule.put(androidResourceModule.targetKey, androidResourceModule);
+      String moduleName = moduleNameForAndroidModule(androidResourceModule.targetKey);
+      Module module = moduleEditor.createModule(moduleName, StdModuleTypes.JAVA);
+      AndroidFacetModuleCustomizer.createAndroidFacet(module);
+    }
+
+    // Configure android resource modules
+    int totalOrderEntries = 0;
+    for (AndroidResourceModule androidResourceModule : targetToAndroidResourceModule.values()) {
+      TargetIdeInfo target = blazeProjectData.targetMap.get(androidResourceModule.targetKey);
+      AndroidIdeInfo androidIdeInfo = target.androidIdeInfo;
+      assert androidIdeInfo != null;
+
+      String moduleName = moduleNameForAndroidModule(target.key);
+      Module module = moduleEditor.findModule(moduleName);
+      assert module != null;
+      ModifiableRootModel modifiableRootModel = moduleEditor.editModule(module);
+
+      Collection<File> resources =
+          blazeProjectData.artifactLocationDecoder.decodeAll(androidResourceModule.resources);
+      ResourceModuleContentRootCustomizer.setupContentRoots(modifiableRootModel, resources);
+
+      for (TargetKey resourceDependency : androidResourceModule.transitiveResourceDependencies) {
+        if (!targetToAndroidResourceModule.containsKey(resourceDependency)) {
+          continue;
+        }
+        String dependencyModuleName = moduleNameForAndroidModule(resourceDependency);
+        Module dependency = moduleEditor.findModule(dependencyModuleName);
+        if (dependency == null) {
+          continue;
+        }
+        modifiableRootModel.addModuleOrderEntry(dependency);
+        ++totalOrderEntries;
+      }
+      // Add a dependency from the workspace to the resource module
+      workspaceModifiableModel.addModuleOrderEntry(module);
+    }
+
+    List<TargetIdeInfo> runConfigurationTargets =
+        getRunConfigurationTargets(
+            project, projectViewSet, blazeProjectData, targetToAndroidResourceModule.keySet());
+    for (TargetIdeInfo target : runConfigurationTargets) {
+      TargetKey targetKey = target.key;
+      String moduleName = moduleNameForAndroidModule(targetKey);
+      Module module = moduleEditor.createModule(moduleName, StdModuleTypes.JAVA);
+      AndroidFacetModuleCustomizer.createAndroidFacet(module);
+    }
+
+    int whitelistedGenResources =
+        projectViewSet.listItems(GeneratedAndroidResourcesSection.KEY).size();
+    context.output(
+        PrintOutput.log(
+            String.format(
+                "Android resource module count: %d, run config modules: %d, order entries: %d, "
+                    + "generated resources: %d",
+                syncData.importResult.androidResourceModules.size(),
+                runConfigurationTargets.size(),
+                totalOrderEntries,
+                whitelistedGenResources)));
+  }
+
+  // Collect potential android run configuration targets
+  private static List<TargetIdeInfo> getRunConfigurationTargets(
+      Project project,
+      ProjectViewSet projectViewSet,
+      BlazeProjectData blazeProjectData,
+      Set<TargetKey> androidResourceModules) {
+    List<TargetIdeInfo> result = Lists.newArrayList();
+    Set<Label> runConfigurationModuleTargets = Sets.newHashSet();
+
+    // Get all explicitly mentioned targets
+    // Doing this now will cut down on root changes later
+    for (TargetExpression targetExpression : projectViewSet.listItems(TargetSection.KEY)) {
+      if (!(targetExpression instanceof Label)) {
+        continue;
+      }
+      Label label = (Label) targetExpression;
+      runConfigurationModuleTargets.add(label);
+    }
+    // Get any pre-existing targets
+    for (RunConfiguration runConfiguration :
+        RunManager.getInstance(project).getAllConfigurationsList()) {
+      BlazeAndroidRunConfigurationHandler handler =
+          BlazeAndroidRunConfigurationHandler.getHandlerFrom(runConfiguration);
+      if (handler == null) {
+        continue;
+      }
+      runConfigurationModuleTargets.add(handler.getLabel());
+    }
+
+    for (Label label : runConfigurationModuleTargets) {
+      TargetKey targetKey = TargetKey.forPlainTarget(label);
+      // If it's a resource module, it will already have been created
+      if (androidResourceModules.contains(targetKey)) {
+        continue;
+      }
+      // Ensure the label is a supported android rule that exists
+      TargetIdeInfo target = blazeProjectData.targetMap.get(targetKey);
+      if (target == null) {
+        continue;
+      }
+      if (!target.kindIsOneOf(Kind.ANDROID_BINARY, Kind.ANDROID_TEST)) {
+        continue;
+      }
+      result.add(target);
+    }
+    return result;
   }
 
   /** Ensures a suitable module exists for the given android target. */
@@ -220,7 +217,6 @@
       return module;
     }
 
-    WorkspaceRoot workspaceRoot = WorkspaceRoot.fromProject(project);
     BlazeProjectData blazeProjectData =
         BlazeProjectDataManager.getInstance(project).getBlazeProjectData();
     if (blazeProjectData == null) {
@@ -246,20 +242,10 @@
     BlazeSyncPlugin.ModuleEditor moduleEditor =
         BlazeProjectDataManager.getInstance(project).editModules();
     Module newModule = moduleEditor.createModule(moduleName, StdModuleTypes.JAVA);
-    ModifiableRootModel modifiableRootModel = moduleEditor.editModule(newModule);
-
     ApplicationManager.getApplication()
         .runWriteAction(
             () -> {
-              updateAndroidTargetModule(
-                  project,
-                  workspaceRoot,
-                  blazeProjectData.artifactLocationDecoder,
-                  androidSdkPlatform,
-                  target,
-                  newModule,
-                  modifiableRootModel,
-                  null);
+              AndroidFacetModuleCustomizer.createAndroidFacet(newModule);
               moduleEditor.commit();
             });
     return newModule;
@@ -273,8 +259,122 @@
         .replace(':', '.');
   }
 
+  public static void updateInMemoryState(
+      Project project,
+      WorkspaceRoot workspaceRoot,
+      ProjectViewSet projectViewSet,
+      BlazeProjectData blazeProjectData,
+      Module workspaceModule,
+      boolean isAndroidWorkspace) {
+    LightResourceClassService.Builder rClassBuilder =
+        new LightResourceClassService.Builder(project);
+    AndroidResourceModuleRegistry registry = AndroidResourceModuleRegistry.getInstance(project);
+    registry.clear();
+    if (isAndroidWorkspace) {
+      updateInMemoryState(
+          project,
+          workspaceRoot,
+          projectViewSet,
+          blazeProjectData,
+          workspaceModule,
+          registry,
+          rClassBuilder);
+    }
+    LightResourceClassService.getInstance(project).installRClasses(rClassBuilder);
+  }
+
+  private static void updateInMemoryState(
+      Project project,
+      WorkspaceRoot workspaceRoot,
+      ProjectViewSet projectViewSet,
+      BlazeProjectData blazeProjectData,
+      Module workspaceModule,
+      AndroidResourceModuleRegistry registry,
+      LightResourceClassService.Builder rClassBuilder) {
+    BlazeAndroidSyncData syncData = blazeProjectData.syncState.get(BlazeAndroidSyncData.class);
+    if (syncData == null) {
+      return;
+    }
+    AndroidSdkPlatform androidSdkPlatform = syncData.androidSdkPlatform;
+    if (androidSdkPlatform == null) {
+      return;
+    }
+
+    updateWorkspaceModuleFacetInMemoryState(
+        project, workspaceRoot, workspaceModule, androidSdkPlatform);
+
+    ArtifactLocationDecoder artifactLocationDecoder = blazeProjectData.artifactLocationDecoder;
+    ModuleManager moduleManager = ModuleManager.getInstance(project);
+    for (AndroidResourceModule androidResourceModule :
+        syncData.importResult.androidResourceModules) {
+      TargetIdeInfo target = blazeProjectData.targetMap.get(androidResourceModule.targetKey);
+      String moduleName = moduleNameForAndroidModule(target.key);
+      Module module = moduleManager.findModuleByName(moduleName);
+      registry.put(module, androidResourceModule);
+
+      AndroidIdeInfo androidIdeInfo = target.androidIdeInfo;
+      assert androidIdeInfo != null;
+
+      updateModuleFacetInMemoryState(
+          project,
+          androidSdkPlatform,
+          module,
+          moduleDirectoryForAndroidTarget(workspaceRoot, target),
+          manifestFileForAndroidTarget(
+              artifactLocationDecoder,
+              androidIdeInfo,
+              moduleDirectoryForAndroidTarget(workspaceRoot, target)),
+          androidIdeInfo.resourceJavaPackage,
+          artifactLocationDecoder.decodeAll(androidResourceModule.transitiveResources));
+      rClassBuilder.addRClass(androidIdeInfo.resourceJavaPackage, module);
+    }
+
+    Set<TargetKey> androidResourceModules =
+        syncData
+            .importResult
+            .androidResourceModules
+            .stream()
+            .map(androidResourceModule -> androidResourceModule.targetKey)
+            .collect(toSet());
+    List<TargetIdeInfo> runConfigurationTargets =
+        getRunConfigurationTargets(
+            project, projectViewSet, blazeProjectData, androidResourceModules);
+    for (TargetIdeInfo target : runConfigurationTargets) {
+      String moduleName = moduleNameForAndroidModule(target.key);
+      Module module = moduleManager.findModuleByName(moduleName);
+      AndroidIdeInfo androidIdeInfo = target.androidIdeInfo;
+      assert androidIdeInfo != null;
+      updateModuleFacetInMemoryState(
+          project,
+          androidSdkPlatform,
+          module,
+          moduleDirectoryForAndroidTarget(workspaceRoot, target),
+          manifestFileForAndroidTarget(
+              artifactLocationDecoder,
+              androidIdeInfo,
+              moduleDirectoryForAndroidTarget(workspaceRoot, target)),
+          androidIdeInfo.resourceJavaPackage,
+          ImmutableList.of());
+    }
+  }
+
+  private static File moduleDirectoryForAndroidTarget(
+      WorkspaceRoot workspaceRoot, TargetIdeInfo target) {
+    return workspaceRoot.fileForPath(target.key.label.blazePackage());
+  }
+
+  private static File manifestFileForAndroidTarget(
+      ArtifactLocationDecoder artifactLocationDecoder,
+      AndroidIdeInfo androidIdeInfo,
+      File moduleDirectory) {
+    ArtifactLocation manifestArtifactLocation = androidIdeInfo.manifest;
+    return manifestArtifactLocation != null
+        ? artifactLocationDecoder.decode(manifestArtifactLocation)
+        : new File(moduleDirectory, "AndroidManifest.xml");
+  }
+
   /** Updates the shared workspace module with android info. */
-  private static void updateWorkspaceModule(
+  private static void updateWorkspaceModuleFacetInMemoryState(
       Project project,
       WorkspaceRoot workspaceRoot,
       Module workspaceModule,
@@ -283,8 +383,7 @@
     File manifest = new File(workspaceRoot.directory(), "AndroidManifest.xml");
     String resourceJavaPackage = ":workspace";
     ImmutableList<File> transitiveResources = ImmutableList.of();
-
-    createAndroidModel(
+    updateModuleFacetInMemoryState(
         project,
         androidSdkPlatform,
         workspaceModule,
@@ -294,49 +393,7 @@
         transitiveResources);
   }
 
-  /** Updates a module from an android rule. */
-  private static void updateAndroidTargetModule(
-      Project project,
-      WorkspaceRoot workspaceRoot,
-      ArtifactLocationDecoder artifactLocationDecoder,
-      AndroidSdkPlatform androidSdkPlatform,
-      TargetIdeInfo target,
-      Module module,
-      ModifiableRootModel modifiableRootModel,
-      @Nullable AndroidResourceModule androidResourceModule) {
-
-    Collection<File> resources =
-        androidResourceModule != null
-            ? artifactLocationDecoder.decodeAll(androidResourceModule.resources)
-            : ImmutableList.of();
-    Collection<File> transitiveResources =
-        androidResourceModule != null
-            ? artifactLocationDecoder.decodeAll(androidResourceModule.transitiveResources)
-            : ImmutableList.of();
-
-    AndroidIdeInfo androidIdeInfo = target.androidIdeInfo;
-    assert androidIdeInfo != null;
-
-    File moduleDirectory = workspaceRoot.fileForPath(target.key.label.blazePackage());
-    ArtifactLocation manifestArtifactLocation = androidIdeInfo.manifest;
-    File manifest =
-        manifestArtifactLocation != null
-            ? artifactLocationDecoder.decode(manifestArtifactLocation)
-            : new File(moduleDirectory, "AndroidManifest.xml");
-    String resourceJavaPackage = androidIdeInfo.resourceJavaPackage;
-    ResourceModuleContentRootCustomizer.setupContentRoots(modifiableRootModel, resources);
-
-    createAndroidModel(
-        project,
-        androidSdkPlatform,
-        module,
-        moduleDirectory,
-        manifest,
-        resourceJavaPackage,
-        transitiveResources);
-  }
-
-  private static void createAndroidModel(
+  private static void updateModuleFacetInMemoryState(
       Project project,
       AndroidSdkPlatform androidSdkPlatform,
       Module module,
@@ -344,7 +401,6 @@
       File manifest,
       String resourceJavaPackage,
       Collection<File> transitiveResources) {
-    AndroidFacetModuleCustomizer.createAndroidFacet(module);
     SourceProvider sourceProvider =
         new SourceProviderImpl(module.getName(), manifest, transitiveResources);
     BlazeAndroidModel androidModel =
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 05b52e4..e4ae1b5 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
@@ -17,6 +17,7 @@
 
 import com.google.common.base.Joiner;
 import com.google.common.collect.Lists;
+import com.google.idea.blaze.android.compatibility.Compatibility.AndroidSdkUtils;
 import com.google.idea.blaze.android.projectview.AndroidSdkPlatformSection;
 import com.google.idea.blaze.android.sync.model.AndroidSdkPlatform;
 import com.google.idea.blaze.base.projectview.ProjectViewSet;
@@ -29,7 +30,6 @@
 import javax.annotation.Nullable;
 import org.jetbrains.android.sdk.AndroidPlatform;
 import org.jetbrains.android.sdk.AndroidSdkAdditionalData;
-import org.jetbrains.android.sdk.AndroidSdkUtils;
 
 /** Calculates AndroidSdkPlatform. */
 public class AndroidSdkFromProjectView {
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 933679d..fcbe1f9 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,6 +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.sync.model.AndroidSdkPlatform;
 import com.google.idea.blaze.android.sync.model.BlazeAndroidSyncData;
 import com.google.idea.blaze.base.model.BlazeProjectData;
@@ -26,7 +27,6 @@
 import com.intellij.openapi.project.Project;
 import com.intellij.openapi.projectRoots.Sdk;
 import org.jetbrains.android.sdk.AndroidPlatform;
-import org.jetbrains.android.sdk.AndroidSdkUtils;
 import org.jetbrains.annotations.NotNull;
 import org.jetbrains.annotations.Nullable;
 
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 0a222a7..7eca0c6 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
@@ -90,58 +90,6 @@
   }
 
   @Test
-  public void readAndWriteShouldMigrate() throws Exception {
-    String oldXml =
-        "<?xml version=\"1.0\"?>"
-            + "<configuration blaze-native-debug=\"true\">"
-            + "  <blaze-user-flag>--flag1</blaze-user-flag>"
-            + "  <blaze-user-flag>--flag2</blaze-user-flag>"
-            + "  <option name=\"USE_LAST_SELECTED_DEVICE\" value=\"true\" />"
-            + "  <option name=\"PREFERRED_AVD\" value=\"some avd\" />"
-            + DEBUGGER_STATE_AUTO_RAW_XML
-            + DEBUGGER_STATE_NATIVE_RAW_XML
-            + DEBUGGER_STATE_JAVA_RAW_XML
-            + DEBUGGER_STATE_HYBRID_RAW_XML
-            + DEBUGGER_STATE_BLAZE_AUTO_RAW_XML
-            + "</configuration>";
-    Element oldElement = saxBuilder.build(new StringReader(oldXml)).getRootElement();
-
-    state.readExternal(oldElement);
-    Element migratedElement = new Element("configuration");
-    state.writeExternal(migratedElement);
-
-    assertThat(migratedElement.getChildren()).hasSize(4);
-    assertThat(migratedElement.getAttribute("blaze-native-debug").getValue()).isEqualTo("true");
-
-    List<Element> flagElements = migratedElement.getChildren("blaze-user-flag");
-    assertThat(flagElements).hasSize(2);
-    assertThat(flagElements.get(0).getText()).isEqualTo("--flag1");
-    assertThat(flagElements.get(1).getText()).isEqualTo("--flag2");
-
-    Element deployTargetStatesElement = migratedElement.getChild("android-deploy-target-states");
-    assertThat(xmlOutputter.outputString(deployTargetStatesElement))
-        .isEqualTo(formatRawXml(DEPLOY_TARGET_STATES_RAW_XML));
-
-    Element debuggerStatesElement = migratedElement.getChild("android-debugger-states");
-    assertThat(debuggerStatesElement.getChildren()).hasSize(5);
-    Element debuggerStateElement = debuggerStatesElement.getChild("Auto");
-    assertThat(xmlOutputter.outputString(debuggerStateElement))
-        .isEqualTo(formatRawXml(DEBUGGER_STATE_AUTO_RAW_XML));
-    debuggerStateElement = debuggerStatesElement.getChild("Native");
-    assertThat(xmlOutputter.outputString(debuggerStateElement))
-        .isEqualTo(formatRawXml(DEBUGGER_STATE_NATIVE_RAW_XML));
-    debuggerStateElement = debuggerStatesElement.getChild("Java");
-    assertThat(xmlOutputter.outputString(debuggerStateElement))
-        .isEqualTo(formatRawXml(DEBUGGER_STATE_JAVA_RAW_XML));
-    debuggerStateElement = debuggerStatesElement.getChild("Hybrid");
-    assertThat(xmlOutputter.outputString(debuggerStateElement))
-        .isEqualTo(formatRawXml(DEBUGGER_STATE_HYBRID_RAW_XML));
-    debuggerStateElement = debuggerStatesElement.getChild("BlazeAuto");
-    assertThat(xmlOutputter.outputString(debuggerStateElement))
-        .isEqualTo(formatRawXml(DEBUGGER_STATE_BLAZE_AUTO_RAW_XML));
-  }
-
-  @Test
   public void readAndWriteShouldRemoveExtraElements() throws Exception {
     String oldXml =
         "<?xml version=\"1.0\"?>"
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 76cd2e5..05de9a5 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
@@ -63,7 +63,6 @@
     state.setMode(BlazeAndroidBinaryRunConfigurationState.LAUNCH_SPECIFIC_ACTIVITY);
     state.setMobileInstall(true);
     state.setUseSplitApksIfPossible(false);
-    state.setInstantRun(true);
     state.setUseWorkProfileIfPresent(true);
     state.setUserId(2);
     state.setDeepLink("http://deeplink");
@@ -83,7 +82,6 @@
         .isEqualTo(BlazeAndroidBinaryRunConfigurationState.LAUNCH_SPECIFIC_ACTIVITY);
     assertThat(readState.mobileInstall()).isTrue();
     assertThat(readState.useSplitApksIfPossible()).isFalse();
-    assertThat(readState.instantRun()).isTrue();
     assertThat(readState.useWorkProfileIfPresent()).isTrue();
     assertThat(readState.getUserId()).isEqualTo(2);
     assertThat(readState.getDeepLink()).isEqualTo("http://deeplink");
@@ -107,7 +105,6 @@
     assertThat(readState.getMode()).isEqualTo(state.getMode());
     assertThat(readState.mobileInstall()).isEqualTo(state.mobileInstall());
     assertThat(readState.useSplitApksIfPossible()).isEqualTo(state.useSplitApksIfPossible());
-    assertThat(readState.instantRun()).isEqualTo(state.instantRun());
     assertThat(readState.useWorkProfileIfPresent()).isEqualTo(state.useWorkProfileIfPresent());
     assertThat(readState.getUserId()).isEqualTo(state.getUserId());
     assertThat(readState.getDeepLink()).isEqualTo(state.getDeepLink());
@@ -125,7 +122,6 @@
     state.setMode(BlazeAndroidBinaryRunConfigurationState.LAUNCH_SPECIFIC_ACTIVITY);
     state.setMobileInstall(true);
     state.setUseSplitApksIfPossible(false);
-    state.setInstantRun(true);
     state.setUseWorkProfileIfPresent(true);
     state.setUserId(2);
     state.setDeepLink("http://deeplink");
@@ -151,7 +147,6 @@
     state.setMode(BlazeAndroidBinaryRunConfigurationState.LAUNCH_SPECIFIC_ACTIVITY);
     state.setMobileInstall(true);
     state.setUseSplitApksIfPossible(false);
-    state.setInstantRun(true);
     state.setUseWorkProfileIfPresent(true);
     state.setUserId(2);
     // We don't test DeepLink because it is not exposed in the editor.
@@ -171,7 +166,6 @@
     assertThat(readState.getMode()).isEqualTo(state.getMode());
     assertThat(readState.mobileInstall()).isEqualTo(state.mobileInstall());
     assertThat(readState.useSplitApksIfPossible()).isEqualTo(state.useSplitApksIfPossible());
-    assertThat(readState.instantRun()).isEqualTo(state.instantRun());
     assertThat(readState.useWorkProfileIfPresent()).isEqualTo(state.useWorkProfileIfPresent());
     assertThat(readState.getUserId()).isEqualTo(state.getUserId());
     // We don't test DeepLink because it is not exposed in the editor.
@@ -197,7 +191,6 @@
     assertThat(readState.getMode()).isEqualTo(state.getMode());
     assertThat(readState.mobileInstall()).isEqualTo(state.mobileInstall());
     assertThat(readState.useSplitApksIfPossible()).isEqualTo(state.useSplitApksIfPossible());
-    assertThat(readState.instantRun()).isEqualTo(state.instantRun());
     assertThat(readState.useWorkProfileIfPresent()).isEqualTo(state.useWorkProfileIfPresent());
     assertThat(readState.getUserId()).isEqualTo(state.getUserId());
     // We don't test DeepLink because it is not exposed in the editor.
diff --git a/aswb/tests/unittests/com/google/idea/blaze/android/sync/importer/model/idea/AndroidResourceModuleRegistryTest.java b/aswb/tests/unittests/com/google/idea/blaze/android/sync/importer/model/idea/AndroidResourceModuleRegistryTest.java
new file mode 100644
index 0000000..9a8e66c
--- /dev/null
+++ b/aswb/tests/unittests/com/google/idea/blaze/android/sync/importer/model/idea/AndroidResourceModuleRegistryTest.java
@@ -0,0 +1,117 @@
+/*
+ * 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.sync.importer.model.idea;
+
+import static com.google.common.truth.Truth.THROW_ASSERTION_ERROR;
+import static com.google.common.truth.Truth.assertThat;
+import static org.mockito.Mockito.mock;
+
+import com.google.idea.blaze.android.sync.model.AndroidResourceModule;
+import com.google.idea.blaze.android.sync.model.AndroidResourceModuleRegistry;
+import com.google.idea.blaze.base.BlazeTestCase;
+import com.google.idea.blaze.base.ideinfo.TargetKey;
+import com.google.idea.blaze.base.model.primitives.Label;
+import com.intellij.openapi.module.Module;
+import org.jetbrains.annotations.NotNull;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** Unit tests for {@link AndroidResourceModuleRegistry}. */
+@RunWith(JUnit4.class)
+public class AndroidResourceModuleRegistryTest extends BlazeTestCase {
+  private AndroidResourceModuleRegistry registry;
+
+  @Override
+  protected void initTest(
+      @NotNull Container applicationServices, @NotNull Container projectServices) {
+    super.initTest(applicationServices, projectServices);
+    projectServices.register(
+        AndroidResourceModuleRegistry.class, new AndroidResourceModuleRegistry());
+
+    registry = AndroidResourceModuleRegistry.getInstance(getProject());
+  }
+
+  @Test
+  public void testPutAndGet() {
+    Module moduleOne = mock(Module.class);
+    Module moduleTwo = mock(Module.class);
+    Module moduleThree = mock(Module.class);
+    AndroidResourceModule resourceModuleOne =
+        AndroidResourceModule.builder(TargetKey.forPlainTarget(new Label("//foo/bar:one"))).build();
+    AndroidResourceModule resourceModuleTwo =
+        AndroidResourceModule.builder(TargetKey.forPlainTarget(new Label("//foo/bar:two"))).build();
+    AndroidResourceModule resourceModuleThree =
+        AndroidResourceModule.builder(TargetKey.forPlainTarget(new Label("//foo/bar:three")))
+            .build();
+    registry.put(moduleOne, resourceModuleOne);
+    registry.put(moduleTwo, resourceModuleTwo);
+    registry.put(moduleThree, resourceModuleThree);
+
+    assertThat(registry.get(moduleOne)).isEqualTo(resourceModuleOne);
+    assertThat(registry.get(moduleTwo)).isEqualTo(resourceModuleTwo);
+    assertThat(registry.get(moduleThree)).isEqualTo(resourceModuleThree);
+    assertThat(registry.getTargetKey(moduleOne)).isEqualTo(resourceModuleOne.targetKey);
+    assertThat(registry.getTargetKey(moduleTwo)).isEqualTo(resourceModuleTwo.targetKey);
+    assertThat(registry.getTargetKey(moduleThree)).isEqualTo(resourceModuleThree.targetKey);
+    assertThat(registry.getLabel(moduleOne)).isEqualTo(resourceModuleOne.targetKey.label);
+    assertThat(registry.getLabel(moduleTwo)).isEqualTo(resourceModuleTwo.targetKey.label);
+    assertThat(registry.getLabel(moduleThree)).isEqualTo(resourceModuleThree.targetKey.label);
+  }
+
+  @Test
+  public void testPutSameKeyDifferentValues() {
+    Module module = mock(Module.class);
+    AndroidResourceModule resourceModuleOne =
+        AndroidResourceModule.builder(TargetKey.forPlainTarget(new Label("//foo/bar:one"))).build();
+    AndroidResourceModule resourceModuleTwo =
+        AndroidResourceModule.builder(TargetKey.forPlainTarget(new Label("//foo/bar:two"))).build();
+    registry.put(module, resourceModuleOne);
+    registry.put(module, resourceModuleTwo);
+    assertThat(registry.get(module)).isEqualTo(resourceModuleTwo);
+  }
+
+  @Test
+  public void testPutDifferentKeysSameValue() {
+    Module moduleOne = mock(Module.class);
+    Module moduleTwo = mock(Module.class);
+    AndroidResourceModule resourceModule =
+        AndroidResourceModule.builder(TargetKey.forPlainTarget(new Label("//foo/bar:one"))).build();
+    registry.put(moduleOne, resourceModule);
+    try {
+      registry.put(moduleTwo, resourceModule);
+      THROW_ASSERTION_ERROR.fail("Expected IllegalArgumentException");
+    } catch (IllegalArgumentException ignored) {
+      // ignored
+    }
+    assertThat(registry.get(moduleOne)).isEqualTo(resourceModule);
+    assertThat(registry.get(moduleTwo)).isNull();
+  }
+
+  @Test
+  public void testGetNull() {
+    assertThat(registry.get(null)).isNull();
+    assertThat(registry.getTargetKey(null)).isNull();
+    assertThat(registry.getLabel(null)).isNull();
+  }
+
+  @Test
+  public void testGetWithoutPut() {
+    assertThat(registry.get(mock(Module.class))).isNull();
+    assertThat(registry.getTargetKey(mock(Module.class))).isNull();
+    assertThat(registry.getLabel(mock(Module.class))).isNull();
+  }
+}
diff --git a/base/BUILD b/base/BUILD
index 6902fd5..850f1dd 100644
--- a/base/BUILD
+++ b/base/BUILD
@@ -6,11 +6,13 @@
     resources = glob(["resources/**/*"]),
     visibility = ["//visibility:public"],
     deps = [
+        "//common/actionhelper",
         "//common/binaryhelper",
         "//common/experiments",
         "//common/formatter",
         "//intellij_platform_sdk:plugin_api",
         "//proto_deps",
+        "//sdkcompat",
         "@jsr305_annotations//jar",
     ],
 )
@@ -29,6 +31,7 @@
     deps = [
         "//base",
         "//intellij_platform_sdk:plugin_api_for_tests",
+        "//sdkcompat",
         "@jsr305_annotations//jar",
         "@junit//jar",
     ],
diff --git a/base/src/META-INF/blaze-base.xml b/base/src/META-INF/blaze-base.xml
index 1e12599..34d33f4 100644
--- a/base/src/META-INF/blaze-base.xml
+++ b/base/src/META-INF/blaze-base.xml
@@ -15,46 +15,86 @@
   -->
 <idea-plugin>
   <actions>
-    <action id="MakeBlazeProject" class="com.google.idea.blaze.base.actions.BlazeMakeProjectAction" use-shortcut-of="CompileDirty" icon="AllIcons.Actions.Compile">
+    <action id="MakeBlazeProject"
+      class="com.google.idea.blaze.base.actions.BlazeMakeProjectAction"
+      text="Compile Project"
+      use-shortcut-of="CompileDirty"
+      icon="AllIcons.Actions.Compile">
     </action>
-    <action id="MakeBlazeModule" class="com.google.idea.blaze.base.actions.BlazeCompileFileAction">
+    <action id="MakeBlazeModule"
+      class="com.google.idea.blaze.base.actions.BlazeCompileFileAction"
+      text="Compile File">
     </action>
-    <action id="Blaze.IncrementalSyncProject" class="com.google.idea.blaze.base.sync.actions.IncrementalSyncProjectAction" icon="BlazeIcons.Blaze">
+    <action id="Blaze.IncrementalSyncProject"
+      class="com.google.idea.blaze.base.sync.actions.IncrementalSyncProjectAction"
+      text="Sync Project with BUILD Files"
+      icon="BlazeIcons.Blaze">
     </action>
-    <action id="Blaze.FullSyncProject" class="com.google.idea.blaze.base.sync.actions.FullSyncProjectAction" icon="BlazeIcons.BlazeSlow">
+    <action id="Blaze.FullSyncProject"
+      class="com.google.idea.blaze.base.sync.actions.FullSyncProjectAction"
+      text="Non-Incrementally Sync Project with BUILD Files"
+      icon="BlazeIcons.BlazeSlow">
     </action>
-    <action id="Blaze.SyncWorkingSet" class="com.google.idea.blaze.base.sync.actions.SyncWorkingSetAction" icon="BlazeIcons.Blaze" text="Sync Working Set">
+    <action id="Blaze.SyncWorkingSet"
+      class="com.google.idea.blaze.base.sync.actions.SyncWorkingSetAction"
+      text="Sync Working Set"
+      icon="BlazeIcons.Blaze">
     </action>
-    <action id="Blaze.ExpandSyncToWorkingSet" class="com.google.idea.blaze.base.sync.actions.ExpandSyncToWorkingSetAction" text="Expand Sync to Working Set">
+    <action id="Blaze.ExpandSyncToWorkingSet"
+      class="com.google.idea.blaze.base.sync.actions.ExpandSyncToWorkingSetAction"
+      text="Expand Sync to Working Set">
     </action>
-    <action id="Blaze.ShowPerformanceWarnings" class="com.google.idea.blaze.base.sync.actions.ShowPerformanceWarningsToggleAction" text="Show Performance Warnings">
+    <action id="Blaze.ShowPerformanceWarnings"
+      class="com.google.idea.blaze.base.sync.actions.ShowPerformanceWarningsToggleAction"
+      text="Show Performance Warnings">
     </action>
-    <action id="Blaze.EditProjectView" class="com.google.idea.blaze.base.settings.ui.EditProjectViewAction" text="Edit Project View..." icon="BlazeIcons.Blaze">
+    <action id="Blaze.EditProjectView"
+      class="com.google.idea.blaze.base.settings.ui.EditProjectViewAction"
+      text="Open Project View Files">
     </action>
-
     <action class="com.google.idea.blaze.base.buildmap.OpenCorrespondingBuildFile"
-            id="Blaze.OpenCorrespondingBuildFile"
-            icon="BlazeIcons.Blaze"
-            text="Open Corresponding BUILD File">
+      id="Blaze.OpenCorrespondingBuildFile"
+      text="Open Corresponding BUILD File">
     </action>
     <action class="com.google.idea.blaze.base.sync.actions.PartialSyncAction"
-            id="Blaze.PartialSync"
-            icon="BlazeIcons.Blaze">
+      id="Blaze.PartialSync"
+      text="Partially Sync File"
+      icon="BlazeIcons.Blaze">
     </action>
+    <action id="Blaze.ExportRunConfigurations"
+      class="com.google.idea.blaze.base.run.exporter.ExportRunConfigurationAction"
+      text="Export Run Configurations"
+      icon="AllIcons.Actions.Export">
+    </action>
+    <action id="Blaze.NewPackageAction"
+      class="com.google.idea.blaze.base.ide.NewBlazePackageAction"
+      text="New Package"/>
+    <action id="Blaze.NewRuleAction"
+      class="com.google.idea.blaze.base.ide.NewBlazeRuleAction"
+      text="New Rule"
+      popup="true"/>
 
     <group id="Blaze.MainMenuActionGroup" class="com.google.idea.blaze.base.actions.BlazeMenuGroup">
       <add-to-group group-id="MainMenu" anchor="before" relative-to-action="HelpMenu"/>
-      <reference id="MakeBlazeProject"/>
-      <reference id="MakeBlazeModule"/>
-      <separator/>
       <reference id="Blaze.EditProjectView"/>
+      <group id ="Blaze.SyncMenuGroup" text="Sync" popup="true">
+        <reference id="Blaze.IncrementalSyncProject"/>
+        <reference id="Blaze.FullSyncProject"/>
+        <reference id="Blaze.SyncWorkingSet"/>
+        <reference id="Blaze.PartialSync"/>
+        <reference id="Blaze.ExpandSyncToWorkingSet"/>
+        <reference id="Blaze.ShowPerformanceWarnings"/>
+      </group>
+      <group id="Blaze.BuildMenuGroup" text="Build" popup="true">
+        <reference id="MakeBlazeProject"/>
+        <reference id="MakeBlazeModule"/>
+      </group>
+      <!--Add popup groups anchored after this bookmark-->
+      <group id="Blaze.MenuGroupsBookmark"/>
       <separator/>
-      <reference id="Blaze.IncrementalSyncProject"/>
-      <reference id="Blaze.FullSyncProject"/>
-      <reference id="Blaze.SyncWorkingSet"/>
-      <reference id="Blaze.PartialSync"/>
-      <reference id="Blaze.ExpandSyncToWorkingSet"/>
-      <reference id="Blaze.ShowPerformanceWarnings"/>
+      <reference id="Blaze.ExportRunConfigurations"/>
+      <!--Add single menu items anchored after this bookmark-->
+      <group id="Blaze.MenuFooter"/>
     </group>
 
     <group id="Blaze.MainToolBarActionGroup">
@@ -65,8 +105,8 @@
 
     <group id="Blaze.NewActions" text="Edit Blaze structure" description="Create new Blaze packages, rules, etc.">
       <add-to-group group-id="NewGroup" anchor="first"/>
-      <action id="Blaze.NewPackageAction" class="com.google.idea.blaze.base.ide.NewBlazePackageAction" popup="true"/>
-      <action id="Blaze.NewRuleAction" class="com.google.idea.blaze.base.ide.NewBlazeRuleAction" popup="true"/>
+      <reference id="Blaze.NewPackageAction"/>
+      <reference id="Blaze.NewRuleAction"/>
       <separator/>
     </group>
 
@@ -137,6 +177,8 @@
     <projectService serviceImplementation="com.google.idea.blaze.base.buildmap.FileToBuildMap"/>
     <projectService serviceInterface="com.google.idea.blaze.base.targetmaps.SourceToTargetMap"
                     serviceImplementation="com.google.idea.blaze.base.targetmaps.SourceToTargetMapImpl"/>
+    <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"/>
@@ -147,6 +189,8 @@
     <applicationService serviceInterface="com.google.idea.blaze.base.prefetch.PrefetchService"
                         serviceImplementation="com.google.idea.blaze.base.prefetch.PrefetchServiceImpl"/>
     <applicationService serviceImplementation="com.google.idea.blaze.base.wizard2.BlazeWizardUserSettingsStorage"/>
+    <applicationService serviceInterface="com.google.idea.blaze.base.wizard2.BlazeWizardOptionProvider"
+                        serviceImplementation="com.google.idea.blaze.base.wizard2.BazelWizardOptionProvider"/>
     <projectService serviceInterface="com.google.idea.blaze.base.sync.workspace.WorkspacePathResolverProvider"
                     serviceImplementation="com.google.idea.blaze.base.sync.workspace.WorkspacePathResolverProviderImpl"/>
     <configurationType implementation="com.google.idea.blaze.base.run.BlazeCommandRunConfigurationType"/>
@@ -159,6 +203,7 @@
     <stepsBeforeRunProvider implementation="com.google.idea.blaze.base.run.BlazeBeforeRunTaskProvider"/>
     <applicationService serviceInterface="com.google.idea.blaze.base.help.BlazeHelpHandler"
                         serviceImplementation="com.google.idea.blaze.base.help.BlazeHelpHandlerImpl"/>
+    <applicationService serviceImplementation="com.google.idea.blaze.base.actions.BlazeBuildService"/>
 
     <additionalTextAttributes scheme="Default" file="base/resources/colorSchemes/BuildDefault.xml"/>
     <typedHandler implementation="com.google.idea.blaze.base.lang.buildfile.completion.BuildCompletionAutoPopupHandler"/>
@@ -254,8 +299,6 @@
     <extensionPoint qualifiedName="com.google.idea.blaze.PsiFileProvider" interface="com.google.idea.blaze.base.lang.buildfile.search.PsiFileProvider"/>
     <extensionPoint qualifiedName="com.google.idea.blaze.VcsHandler"
                     interface="com.google.idea.blaze.base.vcs.BlazeVcsHandler"/>
-    <extensionPoint qualifiedName="com.google.idea.blaze.BlazeWizardOptionProvider"
-                    interface="com.google.idea.blaze.base.wizard2.BlazeWizardOptionProvider"/>
     <extensionPoint qualifiedName="com.google.idea.blaze.DefaultSdkProvider"
                     interface="com.google.idea.blaze.base.sync.sdk.DefaultSdkProvider"/>
     <extensionPoint qualifiedName="com.google.idea.blaze.BuildFlagsProvider" interface="com.google.idea.blaze.base.command.BuildFlagsProvider"/>
@@ -269,6 +312,7 @@
     <extensionPoint qualifiedName="com.google.idea.blaze.TestTargetHeuristic" interface="com.google.idea.blaze.base.run.TestTargetHeuristic"/>
     <extensionPoint qualifiedName="com.google.idea.blaze.ProjectDataDirectoryValidator" interface="com.google.idea.blaze.base.wizard2.ProjectDataDirectoryValidator"/>
     <extensionPoint qualifiedName="com.google.idea.blaze.AspectStrategyProvider" interface="com.google.idea.blaze.base.sync.aspects.strategy.AspectStrategyProvider"/>
+    <extensionPoint qualifiedName="com.google.idea.blaze.DistributedExecutorSupport" interface="com.google.idea.blaze.base.run.DistributedExecutorSupport"/>
   </extensionPoints>
 
   <extensions defaultExtensionNs="com.google.idea.blaze">
@@ -276,9 +320,7 @@
     <SyncListener implementation="com.google.idea.blaze.base.sync.status.BlazeSyncStatusListener"/>
     <SyncListener implementation="com.google.idea.blaze.base.run.testmap.TestTargetFilterImpl$ClearTestMap"/>
     <SyncListener implementation="com.google.idea.blaze.base.targetmaps.SourceToTargetMapImpl$ClearSourceToTargetMap"/>
-    <SyncListener implementation="com.google.idea.blaze.base.lang.buildfile.language.semantics.BuildLanguageSpecProviderImpl"/>
     <SyncPlugin implementation="com.google.idea.blaze.base.lang.buildfile.sync.BuildLangSyncPlugin"/>
-    <BlazeWizardOptionProvider implementation="com.google.idea.blaze.base.wizard2.BazelWizardOptionProvider"/>
     <BuildFlagsProvider implementation="com.google.idea.blaze.base.command.BuildFlagsProviderImpl"/>
     <VcsHandler implementation="com.google.idea.blaze.base.vcs.git.GitBlazeVcsHandler"/>
     <VcsHandler implementation="com.google.idea.blaze.base.vcs.FallbackBlazeVcsHandler" order="last" id="fallback"/>
diff --git a/base/src/com/google/idea/blaze/base/actions/BlazeAction.java b/base/src/com/google/idea/blaze/base/actions/BlazeAction.java
deleted file mode 100644
index ed31396..0000000
--- a/base/src/com/google/idea/blaze/base/actions/BlazeAction.java
+++ /dev/null
@@ -1,59 +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.actions;
-
-import com.google.idea.blaze.base.settings.Blaze;
-import com.intellij.openapi.actionSystem.AnAction;
-import com.intellij.openapi.actionSystem.AnActionEvent;
-import com.intellij.openapi.project.Project;
-import javax.swing.Icon;
-import org.jetbrains.annotations.NotNull;
-import org.jetbrains.annotations.Nullable;
-
-/** Base class action that hides for non-blaze projects. */
-public abstract class BlazeAction extends AnAction {
-  protected BlazeAction() {}
-
-  protected BlazeAction(Icon icon) {
-    super(icon);
-  }
-
-  protected BlazeAction(@Nullable String text) {
-    super(text);
-  }
-
-  protected BlazeAction(@Nullable String text, @Nullable String description, @Nullable Icon icon) {
-    super(text, description, icon);
-  }
-
-  @Override
-  public final void update(AnActionEvent e) {
-    if (!isBlazeProject(e)) {
-      e.getPresentation().setEnabledAndVisible(false);
-      return;
-    }
-
-    e.getPresentation().setEnabledAndVisible(true);
-    doUpdate(e);
-  }
-
-  protected void doUpdate(@NotNull AnActionEvent e) {}
-
-  private static boolean isBlazeProject(@NotNull AnActionEvent e) {
-    Project project = e.getProject();
-    return project != null && Blaze.isBlazeProject(project);
-  }
-}
diff --git a/base/src/com/google/idea/blaze/base/actions/BlazeBuildService.java b/base/src/com/google/idea/blaze/base/actions/BlazeBuildService.java
new file mode 100644
index 0000000..4da44f1
--- /dev/null
+++ b/base/src/com/google/idea/blaze/base/actions/BlazeBuildService.java
@@ -0,0 +1,141 @@
+/*
+ * 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.actions;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.collect.ImmutableCollection;
+import com.google.common.collect.Lists;
+import com.google.idea.blaze.base.async.executor.BlazeExecutor;
+import com.google.idea.blaze.base.experiments.ExperimentScope;
+import com.google.idea.blaze.base.filecache.FileCaches;
+import com.google.idea.blaze.base.metrics.Action;
+import com.google.idea.blaze.base.model.BlazeProjectData;
+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.projectview.section.sections.TargetSection;
+import com.google.idea.blaze.base.scope.BlazeContext;
+import com.google.idea.blaze.base.scope.ScopedTask;
+import com.google.idea.blaze.base.scope.scopes.BlazeConsoleScope;
+import com.google.idea.blaze.base.scope.scopes.IdeaLogScope;
+import com.google.idea.blaze.base.scope.scopes.IssuesScope;
+import com.google.idea.blaze.base.scope.scopes.LoggedTimingScope;
+import com.google.idea.blaze.base.scope.scopes.NotificationScope;
+import com.google.idea.blaze.base.scope.scopes.TimingScope;
+import com.google.idea.blaze.base.settings.Blaze;
+import com.google.idea.blaze.base.sync.aspects.BlazeIdeInterface;
+import com.google.idea.blaze.base.sync.data.BlazeProjectDataManager;
+import com.google.idea.blaze.base.util.SaveUtil;
+import com.intellij.openapi.components.ServiceManager;
+import com.intellij.openapi.project.Project;
+import java.util.List;
+
+/** Utility to build various collections of targets. */
+public class BlazeBuildService {
+  public static BlazeBuildService getInstance() {
+    return ServiceManager.getService(BlazeBuildService.class);
+  }
+
+  public void buildFile(Project project, String fileName, ImmutableCollection<Label> targets) {
+    if (project == null || !Blaze.isBlazeProject(project) || fileName == null) {
+      return;
+    }
+    buildTargetExpressions(
+        project,
+        Lists.newArrayList(targets),
+        ProjectViewManager.getInstance(project).getProjectViewSet(),
+        new LoggedTimingScope(project, Action.MAKE_MODULE_TOTAL_TIME),
+        new NotificationScope(
+            project,
+            "Make",
+            "Make " + fileName,
+            "Make " + fileName + " completed successfully",
+            "Make " + fileName + " failed"));
+  }
+
+  public void buildProject(Project project) {
+    if (project == null || !Blaze.isBlazeProject(project)) {
+      return;
+    }
+    ProjectViewSet projectViewSet = ProjectViewManager.getInstance(project).getProjectViewSet();
+    if (projectViewSet == null) {
+      return;
+    }
+    buildTargetExpressions(
+        project,
+        projectViewSet.listItems(TargetSection.KEY),
+        projectViewSet,
+        new LoggedTimingScope(project, Action.MAKE_PROJECT_TOTAL_TIME),
+        new NotificationScope(
+            project,
+            "Make",
+            "Make project",
+            "Make project completed successfully",
+            "Make project failed"));
+  }
+
+  @VisibleForTesting
+  void buildTargetExpressions(
+      Project project,
+      List<TargetExpression> targets,
+      ProjectViewSet projectViewSet,
+      LoggedTimingScope loggedTimingScope,
+      NotificationScope notificationScope) {
+    if (targets.isEmpty() || projectViewSet == null) {
+      return;
+    }
+    BlazeProjectData blazeProjectData =
+        BlazeProjectDataManager.getInstance(project).getBlazeProjectData();
+    if (blazeProjectData == null) {
+      return;
+    }
+    BlazeExecutor.submitTask(
+        project,
+        new ScopedTask() {
+          @Override
+          public void execute(BlazeContext context) {
+            context
+                .push(new ExperimentScope())
+                .push(new BlazeConsoleScope.Builder(project).build())
+                .push(new IssuesScope(project))
+                .push(new IdeaLogScope())
+                .push(new TimingScope("Make"))
+                .push(loggedTimingScope)
+                .push(notificationScope);
+
+            WorkspaceRoot workspaceRoot = WorkspaceRoot.fromProject(project);
+            BlazeIdeInterface blazeIdeInterface = BlazeIdeInterface.getInstance();
+
+            SaveUtil.saveAllFiles();
+            BlazeIdeInterface.BuildResult buildResult =
+                blazeIdeInterface.compileIdeArtifacts(
+                    project,
+                    context,
+                    workspaceRoot,
+                    projectViewSet,
+                    blazeProjectData.blazeVersionData,
+                    targets);
+            FileCaches.refresh(project);
+
+            if (buildResult != BlazeIdeInterface.BuildResult.SUCCESS) {
+              context.setHasError();
+            }
+          }
+        });
+  }
+}
diff --git a/base/src/com/google/idea/blaze/base/actions/BlazeCompileFileAction.java b/base/src/com/google/idea/blaze/base/actions/BlazeCompileFileAction.java
index 00ad470..130de1b 100644
--- a/base/src/com/google/idea/blaze/base/actions/BlazeCompileFileAction.java
+++ b/base/src/com/google/idea/blaze/base/actions/BlazeCompileFileAction.java
@@ -17,65 +17,29 @@
 
 import com.google.common.collect.ImmutableCollection;
 import com.google.common.collect.ImmutableList;
-import com.google.common.collect.Lists;
-import com.google.idea.blaze.base.async.executor.BlazeExecutor;
-import com.google.idea.blaze.base.experiments.ExperimentScope;
-import com.google.idea.blaze.base.filecache.FileCaches;
-import com.google.idea.blaze.base.metrics.Action;
-import com.google.idea.blaze.base.model.BlazeProjectData;
 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.scope.BlazeContext;
-import com.google.idea.blaze.base.scope.ScopedTask;
-import com.google.idea.blaze.base.scope.scopes.BlazeConsoleScope;
-import com.google.idea.blaze.base.scope.scopes.IdeaLogScope;
-import com.google.idea.blaze.base.scope.scopes.IssuesScope;
-import com.google.idea.blaze.base.scope.scopes.LoggedTimingScope;
-import com.google.idea.blaze.base.scope.scopes.NotificationScope;
-import com.google.idea.blaze.base.scope.scopes.TimingScope;
-import com.google.idea.blaze.base.sync.aspects.BlazeIdeInterface;
-import com.google.idea.blaze.base.sync.aspects.BlazeIdeInterface.BuildResult;
-import com.google.idea.blaze.base.sync.data.BlazeProjectDataManager;
 import com.google.idea.blaze.base.targetmaps.SourceToTargetMap;
-import com.google.idea.blaze.base.util.SaveUtil;
+import com.google.idea.common.actionhelper.ActionPresentationHelper;
 import com.intellij.openapi.actionSystem.AnActionEvent;
 import com.intellij.openapi.actionSystem.CommonDataKeys;
-import com.intellij.openapi.actionSystem.Presentation;
-import com.intellij.openapi.diagnostic.Logger;
 import com.intellij.openapi.project.Project;
 import com.intellij.openapi.vfs.VirtualFile;
 import java.io.File;
-import java.util.List;
-import org.jetbrains.annotations.NotNull;
 
-class BlazeCompileFileAction extends BlazeAction {
-  private static final Logger LOG = Logger.getInstance(BlazeCompileFileAction.class);
+class BlazeCompileFileAction extends BlazeProjectAction {
 
-  public BlazeCompileFileAction() {
-    super("Compile file");
+  @Override
+  protected void updateForBlazeProject(Project project, AnActionEvent e) {
+    ActionPresentationHelper.of(e)
+        .disableIf(getTargets(e).isEmpty())
+        .setTextWithSubject("Compile File", "Compile %s", e.getData(CommonDataKeys.VIRTUAL_FILE))
+        .disableWithoutSubject()
+        .commit();
   }
 
   @Override
-  protected void doUpdate(@NotNull AnActionEvent e) {
-    // IntelliJ uses different logic for 1 vs many module selection. When many modules are selected
-    // modules with more than 1 content root are ignored
-    // (ProjectViewImpl#moduleBySingleContentRoot).
-    if (getTargets(e).isEmpty()) {
-      Presentation presentation = e.getPresentation();
-      presentation.setEnabled(false);
-    }
-  }
-
-  @Override
-  public void actionPerformed(AnActionEvent e) {
-    Project project = e.getProject();
-    if (project != null) {
-      ImmutableCollection<Label> targets = getTargets(e);
-      buildSourceFile(project, targets);
-    }
+  protected void actionPerformedInBlazeProject(Project project, AnActionEvent e) {
+    BlazeBuildService.getInstance().buildFile(project, getFileName(e), getTargets(e));
   }
 
   private ImmutableCollection<Label> getTargets(AnActionEvent e) {
@@ -88,53 +52,8 @@
     return ImmutableList.of();
   }
 
-  private static void buildSourceFile(
-      @NotNull Project project, @NotNull ImmutableCollection<Label> targets) {
-    BlazeProjectData blazeProjectData =
-        BlazeProjectDataManager.getInstance(project).getBlazeProjectData();
-    if (blazeProjectData == null || targets.isEmpty()) {
-      return;
-    }
-    final ProjectViewSet projectViewSet =
-        ProjectViewManager.getInstance(project).getProjectViewSet();
-    if (projectViewSet == null) {
-      return;
-    }
-    BlazeExecutor.submitTask(
-        project,
-        new ScopedTask() {
-          @Override
-          public void execute(@NotNull BlazeContext context) {
-            context
-                .push(new ExperimentScope())
-                .push(new BlazeConsoleScope.Builder(project).build())
-                .push(new IssuesScope(project))
-                .push(new IdeaLogScope())
-                .push(new TimingScope("Make"))
-                .push(new LoggedTimingScope(project, Action.MAKE_MODULE_TOTAL_TIME))
-                .push(
-                    new NotificationScope(
-                        project,
-                        "Make",
-                        "Make module",
-                        "Make module completed successfully",
-                        "Make module failed"));
-
-            WorkspaceRoot workspaceRoot = WorkspaceRoot.fromProject(project);
-
-            BlazeIdeInterface blazeIdeInterface = BlazeIdeInterface.getInstance();
-            List<TargetExpression> targetExpressions = Lists.newArrayList(targets);
-
-            SaveUtil.saveAllFiles();
-            BuildResult buildResult =
-                blazeIdeInterface.resolveIdeArtifacts(
-                    project, context, workspaceRoot, projectViewSet, targetExpressions);
-            FileCaches.refresh(project);
-
-            if (buildResult != BuildResult.SUCCESS) {
-              context.setHasError();
-            }
-          }
-        });
+  private static String getFileName(AnActionEvent e) {
+    VirtualFile virtualFile = e.getData(CommonDataKeys.VIRTUAL_FILE);
+    return virtualFile == null ? null : virtualFile.getName();
   }
 }
diff --git a/base/src/com/google/idea/blaze/base/actions/BlazeMakeProjectAction.java b/base/src/com/google/idea/blaze/base/actions/BlazeMakeProjectAction.java
index 64964c5..a124759 100644
--- a/base/src/com/google/idea/blaze/base/actions/BlazeMakeProjectAction.java
+++ b/base/src/com/google/idea/blaze/base/actions/BlazeMakeProjectAction.java
@@ -15,102 +15,13 @@
  */
 package com.google.idea.blaze.base.actions;
 
-import com.google.common.collect.Lists;
-import com.google.idea.blaze.base.async.executor.BlazeExecutor;
-import com.google.idea.blaze.base.experiments.ExperimentScope;
-import com.google.idea.blaze.base.filecache.FileCaches;
-import com.google.idea.blaze.base.metrics.Action;
-import com.google.idea.blaze.base.model.BlazeProjectData;
-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.projectview.section.sections.TargetSection;
-import com.google.idea.blaze.base.scope.BlazeContext;
-import com.google.idea.blaze.base.scope.ScopedTask;
-import com.google.idea.blaze.base.scope.scopes.BlazeConsoleScope;
-import com.google.idea.blaze.base.scope.scopes.IdeaLogScope;
-import com.google.idea.blaze.base.scope.scopes.IssuesScope;
-import com.google.idea.blaze.base.scope.scopes.LoggedTimingScope;
-import com.google.idea.blaze.base.scope.scopes.NotificationScope;
-import com.google.idea.blaze.base.scope.scopes.TimingScope;
-import com.google.idea.blaze.base.settings.Blaze;
-import com.google.idea.blaze.base.sync.aspects.BlazeIdeInterface;
-import com.google.idea.blaze.base.sync.aspects.BlazeIdeInterface.BuildResult;
-import com.google.idea.blaze.base.sync.data.BlazeProjectDataManager;
-import com.google.idea.blaze.base.util.SaveUtil;
 import com.intellij.openapi.actionSystem.AnActionEvent;
 import com.intellij.openapi.project.Project;
-import java.util.List;
-import org.jetbrains.annotations.NotNull;
 
-class BlazeMakeProjectAction extends BlazeAction {
-
-  public BlazeMakeProjectAction() {
-    super("Make Project");
-  }
+class BlazeMakeProjectAction extends BlazeProjectAction {
 
   @Override
-  public final void actionPerformed(AnActionEvent e) {
-    Project project = e.getProject();
-    if (project != null && Blaze.isBlazeProject(project)) {
-      buildBlazeProject(project);
-    }
-  }
-
-  protected void buildBlazeProject(@NotNull final Project project) {
-
-    BlazeExecutor.submitTask(
-        project,
-        new ScopedTask() {
-          @Override
-          public void execute(@NotNull BlazeContext context) {
-            context
-                .push(new ExperimentScope())
-                .push(new BlazeConsoleScope.Builder(project).build())
-                .push(new IssuesScope(project))
-                .push(new IdeaLogScope())
-                .push(new TimingScope("Make"))
-                .push(new LoggedTimingScope(project, Action.MAKE_PROJECT_TOTAL_TIME))
-                .push(
-                    new NotificationScope(
-                        project,
-                        "Make",
-                        "Make project",
-                        "Make project completed successfully",
-                        "Make project failed"));
-
-            BlazeProjectData blazeProjectData =
-                BlazeProjectDataManager.getInstance(project).getBlazeProjectData();
-            if (blazeProjectData == null) {
-              return;
-            }
-            SaveUtil.saveAllFiles();
-
-            ProjectViewSet projectViewSet =
-                ProjectViewManager.getInstance(project)
-                    .reloadProjectView(context, blazeProjectData.workspacePathResolver);
-            if (projectViewSet == null) {
-              return;
-            }
-
-            WorkspaceRoot workspaceRoot = WorkspaceRoot.fromProject(project);
-
-            List<TargetExpression> targets = Lists.newArrayList();
-            targets.addAll(projectViewSet.listItems(TargetSection.KEY));
-
-            BlazeIdeInterface blazeIdeInterface = BlazeIdeInterface.getInstance();
-
-            BuildResult buildResult =
-                blazeIdeInterface.resolveIdeArtifacts(
-                    project, context, workspaceRoot, projectViewSet, targets);
-            FileCaches.refresh(project);
-
-            if (buildResult != BuildResult.SUCCESS) {
-              context.setHasError();
-              ;
-            }
-          }
-        });
+  protected void actionPerformedInBlazeProject(Project project, AnActionEvent e) {
+    BlazeBuildService.getInstance().buildProject(project);
   }
 }
diff --git a/base/src/com/google/idea/blaze/base/actions/BlazeProjectAction.java b/base/src/com/google/idea/blaze/base/actions/BlazeProjectAction.java
new file mode 100644
index 0000000..fdf81be
--- /dev/null
+++ b/base/src/com/google/idea/blaze/base/actions/BlazeProjectAction.java
@@ -0,0 +1,86 @@
+/*
+ * 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.actions;
+
+import com.google.idea.blaze.base.settings.Blaze;
+import com.google.idea.blaze.base.settings.Blaze.BuildSystem;
+import com.intellij.openapi.actionSystem.AnAction;
+import com.intellij.openapi.actionSystem.AnActionEvent;
+import com.intellij.openapi.project.Project;
+import javax.annotation.Nullable;
+import javax.swing.Icon;
+
+/** Base class action that hides for non-blaze projects. */
+public abstract class BlazeProjectAction extends AnAction {
+  protected BlazeProjectAction() {}
+
+  protected BlazeProjectAction(Icon icon) {
+    super(icon);
+  }
+
+  protected BlazeProjectAction(@Nullable String text) {
+    super(text);
+  }
+
+  protected BlazeProjectAction(
+      @Nullable String text, @Nullable String description, @Nullable Icon icon) {
+    super(text, description, icon);
+  }
+
+  @Override
+  public final void update(AnActionEvent e) {
+    Project project = e.getProject();
+    if (project == null || !Blaze.isBlazeProject(project)) {
+      e.getPresentation().setEnabledAndVisible(false);
+      return;
+    }
+
+    e.getPresentation().setEnabledAndVisible(true);
+
+    if (!compatibleBuildSystem(project)) {
+      e.getPresentation().setEnabled(false);
+      return;
+    }
+
+    updateForBlazeProject(project, e);
+  }
+
+  @Override
+  public final void actionPerformed(AnActionEvent anActionEvent) {
+    Project project = anActionEvent.getProject();
+    if (project == null) {
+      return;
+    }
+    actionPerformedInBlazeProject(project, anActionEvent);
+  }
+
+  protected void updateForBlazeProject(Project project, AnActionEvent e) {}
+
+  protected abstract void actionPerformedInBlazeProject(Project project, AnActionEvent e);
+
+  private boolean compatibleBuildSystem(Project project) {
+    BuildSystem requiredBuildSystem = requiredBuildSystem();
+    if (requiredBuildSystem == null) {
+      return true;
+    }
+    return Blaze.getBuildSystem(project) == requiredBuildSystem;
+  }
+
+  @Nullable
+  protected BuildSystem requiredBuildSystem() {
+    return null;
+  }
+}
diff --git a/base/src/com/google/idea/blaze/base/actions/BlazeToggleAction.java b/base/src/com/google/idea/blaze/base/actions/BlazeProjectToggleAction.java
similarity index 73%
rename from base/src/com/google/idea/blaze/base/actions/BlazeToggleAction.java
rename to base/src/com/google/idea/blaze/base/actions/BlazeProjectToggleAction.java
index 0413761..270f067 100644
--- a/base/src/com/google/idea/blaze/base/actions/BlazeToggleAction.java
+++ b/base/src/com/google/idea/blaze/base/actions/BlazeProjectToggleAction.java
@@ -19,39 +19,34 @@
 import com.intellij.openapi.actionSystem.AnActionEvent;
 import com.intellij.openapi.actionSystem.ToggleAction;
 import com.intellij.openapi.project.Project;
+import javax.annotation.Nullable;
 import javax.swing.Icon;
-import org.jetbrains.annotations.NotNull;
-import org.jetbrains.annotations.Nullable;
 
 /** Base class toggle action that hides for non-blaze projects. */
-public abstract class BlazeToggleAction extends ToggleAction {
-  protected BlazeToggleAction() {}
+public abstract class BlazeProjectToggleAction extends ToggleAction {
+  protected BlazeProjectToggleAction() {}
 
-  protected BlazeToggleAction(@Nullable String text) {
+  protected BlazeProjectToggleAction(@Nullable String text) {
     super(text);
   }
 
-  protected BlazeToggleAction(
+  protected BlazeProjectToggleAction(
       @Nullable String text, @Nullable String description, @Nullable Icon icon) {
     super(text, description, icon);
   }
 
   @Override
   public final void update(AnActionEvent e) {
-    if (!isBlazeProject(e)) {
+    Project project = e.getProject();
+    if (project == null || !Blaze.isBlazeProject(project)) {
       e.getPresentation().setEnabledAndVisible(false);
       return;
     }
 
     e.getPresentation().setEnabledAndVisible(true);
     super.update(e);
-    doUpdate(e);
+    updateForBlazeProject(project, e);
   }
 
-  protected void doUpdate(@NotNull AnActionEvent e) {}
-
-  private static boolean isBlazeProject(@NotNull AnActionEvent e) {
-    Project project = e.getProject();
-    return project != null && Blaze.isBlazeProject(project);
-  }
+  protected void updateForBlazeProject(Project project, AnActionEvent e) {}
 }
diff --git a/base/src/com/google/idea/blaze/base/async/AsyncUtil.java b/base/src/com/google/idea/blaze/base/async/AsyncUtil.java
deleted file mode 100644
index 153f995..0000000
--- a/base/src/com/google/idea/blaze/base/async/AsyncUtil.java
+++ /dev/null
@@ -1,58 +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.async;
-
-import com.intellij.openapi.application.ApplicationManager;
-import com.intellij.util.ui.UIUtil;
-import org.jetbrains.annotations.NotNull;
-
-/** Async utilities. */
-public class AsyncUtil {
-  public static void executeProjectChangeAction(@NotNull final Runnable task) throws Throwable {
-    final ValueHolder<Throwable> error = new ValueHolder<Throwable>();
-
-    executeOnEdt(
-        new Runnable() {
-          @Override
-          public void run() {
-            ApplicationManager.getApplication()
-                .runWriteAction(
-                    new Runnable() {
-                      @Override
-                      public void run() {
-                        try {
-                          task.run();
-                        } catch (Throwable t) {
-                          error.value = t;
-                        }
-                      }
-                    });
-          }
-        });
-
-    if (error.value != null) {
-      throw error.value;
-    }
-  }
-
-  private static void executeOnEdt(@NotNull Runnable task) {
-    if (ApplicationManager.getApplication().isDispatchThread()) {
-      task.run();
-    } else {
-      UIUtil.invokeAndWaitIfNeeded(task);
-    }
-  }
-}
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 d207ff2..b3d4598 100644
--- a/base/src/com/google/idea/blaze/base/bazel/BazelBuildSystemProvider.java
+++ b/base/src/com/google/idea/blaze/base/bazel/BazelBuildSystemProvider.java
@@ -16,8 +16,10 @@
 package com.google.idea.blaze.base.bazel;
 
 import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
 import com.google.idea.blaze.base.io.FileAttributeProvider;
 import com.google.idea.blaze.base.lang.buildfile.language.semantics.RuleDefinition;
+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.intellij.openapi.fileTypes.ExactFileNameMatcher;
@@ -27,6 +29,7 @@
 
 /** Provides the bazel build system name string. */
 public class BazelBuildSystemProvider implements BuildSystemProvider {
+
   @Override
   public BuildSystem buildSystem() {
     return BuildSystem.Bazel;
@@ -72,4 +75,16 @@
   public FileNameMatcher buildFileMatcher() {
     return new ExactFileNameMatcher("BUILD");
   }
+
+  @Override
+  public void populateBlazeVersionData(
+      BuildSystem buildSystem,
+      WorkspaceRoot workspaceRoot,
+      ImmutableMap<String, String> blazeInfo,
+      BlazeVersionData.Builder builder) {
+    if (buildSystem != BuildSystem.Bazel) {
+      return;
+    }
+    builder.setBazelVersion(BazelVersion.parseVersion(blazeInfo));
+  }
 }
diff --git a/base/src/com/google/idea/blaze/base/bazel/BazelVersion.java b/base/src/com/google/idea/blaze/base/bazel/BazelVersion.java
new file mode 100644
index 0000000..44582cb
--- /dev/null
+++ b/base/src/com/google/idea/blaze/base/bazel/BazelVersion.java
@@ -0,0 +1,109 @@
+/*
+ * 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.bazel;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.base.Objects;
+import com.google.common.collect.ComparisonChain;
+import com.google.idea.blaze.base.command.info.BlazeInfo;
+import com.intellij.openapi.util.text.StringUtil;
+import java.io.Serializable;
+import java.util.Map;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+import javax.annotation.Nullable;
+
+/** Bazel version */
+public class BazelVersion implements Serializable {
+  private static final long serialVersionUID = 1L;
+
+  public static final BazelVersion UNKNOWN = new BazelVersion(0, 0, 0);
+  private static final Pattern PATTERN = Pattern.compile("([[0-9]\\.]+)");
+
+  public final int major;
+  public final int minor;
+  public final int bugfix;
+
+  BazelVersion(int major, int minor, int bugfix) {
+    this.bugfix = bugfix;
+    this.minor = minor;
+    this.major = major;
+  }
+
+  @VisibleForTesting
+  static BazelVersion parseVersion(@Nullable String string) {
+    if (string == null) {
+      return UNKNOWN;
+    }
+    Matcher matcher = PATTERN.matcher(string);
+    if (!matcher.find()) {
+      return UNKNOWN;
+    }
+    try {
+      BazelVersion version = parseVersion(matcher.group(1).split("\\."));
+      if (version == null) {
+        return UNKNOWN;
+      }
+      return version;
+    } catch (Exception e) {
+      return UNKNOWN;
+    }
+  }
+
+  @Nullable
+  private static BazelVersion parseVersion(String[] numbers) {
+    if (numbers.length < 1) {
+      return null;
+    }
+    int major = StringUtil.parseInt(numbers[0], -1);
+    if (major < 0) {
+      return null;
+    }
+    int minor = numbers.length > 1 ? StringUtil.parseInt(numbers[1], 0) : 0;
+    int bugfix = numbers.length > 2 ? StringUtil.parseInt(numbers[2], 0) : 0;
+    return new BazelVersion(major, minor, bugfix);
+  }
+
+  public static BazelVersion parseVersion(Map<String, String> blazeInfo) {
+    return parseVersion(blazeInfo.get(BlazeInfo.RELEASE));
+  }
+
+  @Override
+  public boolean equals(Object o) {
+    if (this == o) {
+      return true;
+    }
+    if (o == null || getClass() != o.getClass()) {
+      return false;
+    }
+    BazelVersion that = (BazelVersion) o;
+    return major == that.major && minor == that.minor && bugfix == that.bugfix;
+  }
+
+  @Override
+  public int hashCode() {
+    return Objects.hashCode(major, minor, bugfix);
+  }
+
+  public boolean isAtLeast(int major, int minor, int bugfix) {
+    return ComparisonChain.start()
+            .compare(this.major, major)
+            .compare(this.minor, minor)
+            .compare(this.bugfix, bugfix)
+            .result()
+        >= 0;
+  }
+}
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 4b47716..6ce5aca 100644
--- a/base/src/com/google/idea/blaze/base/bazel/BuildSystemProvider.java
+++ b/base/src/com/google/idea/blaze/base/bazel/BuildSystemProvider.java
@@ -16,7 +16,9 @@
 package com.google.idea.blaze.base.bazel;
 
 import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
 import com.google.idea.blaze.base.lang.buildfile.language.semantics.RuleDefinition;
+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.intellij.openapi.components.ServiceManager;
@@ -105,4 +107,11 @@
   }
 
   FileNameMatcher buildFileMatcher();
+
+  /** Populates the passed builder with version data. */
+  void populateBlazeVersionData(
+      BuildSystem buildSystem,
+      WorkspaceRoot workspaceRoot,
+      ImmutableMap<String, String> blazeInfo,
+      BlazeVersionData.Builder builder);
 }
diff --git a/base/src/com/google/idea/blaze/base/buildmap/OpenCorrespondingBuildFile.java b/base/src/com/google/idea/blaze/base/buildmap/OpenCorrespondingBuildFile.java
index 2754da2..592b7b4 100644
--- a/base/src/com/google/idea/blaze/base/buildmap/OpenCorrespondingBuildFile.java
+++ b/base/src/com/google/idea/blaze/base/buildmap/OpenCorrespondingBuildFile.java
@@ -16,7 +16,7 @@
 package com.google.idea.blaze.base.buildmap;
 
 import com.google.common.collect.Iterables;
-import com.google.idea.blaze.base.actions.BlazeAction;
+import com.google.idea.blaze.base.actions.BlazeProjectAction;
 import com.google.idea.blaze.base.metrics.Action;
 import com.google.idea.blaze.base.metrics.LoggingService;
 import com.intellij.ide.actions.OpenFileAction;
@@ -29,16 +29,12 @@
 import com.intellij.openapi.vfs.VirtualFile;
 import java.io.File;
 import java.util.Collection;
-import org.jetbrains.annotations.NotNull;
-import org.jetbrains.annotations.Nullable;
+import javax.annotation.Nullable;
 
-class OpenCorrespondingBuildFile extends BlazeAction {
+class OpenCorrespondingBuildFile extends BlazeProjectAction {
+
   @Override
-  public void actionPerformed(AnActionEvent e) {
-    Project project = e.getProject();
-    if (project == null) {
-      return;
-    }
+  protected void actionPerformedInBlazeProject(Project project, AnActionEvent e) {
     VirtualFile virtualFile = e.getData(CommonDataKeys.VIRTUAL_FILE);
     File file = getBuildFile(project, virtualFile);
     if (file == null) {
@@ -49,10 +45,7 @@
   }
 
   @Nullable
-  private File getBuildFile(@Nullable Project project, @Nullable VirtualFile virtualFile) {
-    if (project == null) {
-      return null;
-    }
+  private File getBuildFile(Project project, @Nullable VirtualFile virtualFile) {
     if (virtualFile == null) {
       return null;
     }
@@ -62,12 +55,11 @@
   }
 
   @Override
-  protected void doUpdate(@NotNull AnActionEvent e) {
+  protected void updateForBlazeProject(Project project, AnActionEvent e) {
     Presentation presentation = e.getPresentation();
     DataContext dataContext = e.getDataContext();
     VirtualFile virtualFile = CommonDataKeys.VIRTUAL_FILE.getData(dataContext);
-    Project project = CommonDataKeys.PROJECT.getData(dataContext);
-    boolean visible = (project != null && virtualFile != null);
+    boolean visible = virtualFile != null;
     boolean enabled = getBuildFile(project, virtualFile) != null;
     presentation.setVisible(visible || ActionPlaces.isMainMenuOrActionSearch(e.getPlace()));
     presentation.setEnabled(enabled);
diff --git a/base/src/com/google/idea/blaze/base/command/BlazeFlags.java b/base/src/com/google/idea/blaze/base/command/BlazeFlags.java
index 2338f24..1f24f62 100644
--- a/base/src/com/google/idea/blaze/base/command/BlazeFlags.java
+++ b/base/src/com/google/idea/blaze/base/command/BlazeFlags.java
@@ -41,13 +41,6 @@
   public static final String TEST_OUTPUT_STREAMED = "--test_output=streamed";
   // Filters the unit tests that are run (used with regexp for Java/Robolectric tests).
   public static final String TEST_FILTER = "--test_filter";
-  // Skips checking for output file modifications (reduced statting -> faster).
-  public static final String NO_CHECK_OUTPUTS = "--noexperimental_check_output_files";
-  // Ignores implicit dependencies (e.g. java_library rules depending implicitly on
-  // "//transconsole/tools:aggregate_messages" in order to support translations).
-  public static final String NO_IMPLICIT_DEPS = "--noimplicit_deps";
-  // Ignores host dependencies.
-  public static final String NO_HOST_DEPS = "--nohost_deps";
   // When used with mobile-install, deploys the an app incrementally.
   public static final String INCREMENTAL = "--incremental";
   // When used with mobile-install, deploys the an app incrementally
diff --git a/base/src/com/google/idea/blaze/base/command/BuildFlagsProviderImpl.java b/base/src/com/google/idea/blaze/base/command/BuildFlagsProviderImpl.java
index 290f001..2e40662 100644
--- a/base/src/com/google/idea/blaze/base/command/BuildFlagsProviderImpl.java
+++ b/base/src/com/google/idea/blaze/base/command/BuildFlagsProviderImpl.java
@@ -15,7 +15,6 @@
  */
 package com.google.idea.blaze.base.command;
 
-import static com.google.idea.blaze.base.command.BlazeFlags.NO_CHECK_OUTPUTS;
 import static com.google.idea.blaze.base.command.BlazeFlags.VERSION_WINDOW_FOR_DIRTY_NODE_GC;
 
 import com.google.idea.blaze.base.projectview.ProjectViewSet;
@@ -28,8 +27,6 @@
 
   private static final BoolExperiment experimentUseVersionWindowForDirtyNodeGc =
       new BoolExperiment("ide_build_info.use_version_window_for_dirty_node_gc", false);
-  private static final BoolExperiment experimentNoExperimentalCheckOutputFiles =
-      new BoolExperiment("build.noexperimental_check_output_files", false);
 
   @Override
   public void addBuildFlags(
@@ -37,9 +34,6 @@
     if (experimentUseVersionWindowForDirtyNodeGc.getValue()) {
       flags.add(VERSION_WINDOW_FOR_DIRTY_NODE_GC);
     }
-    if (experimentNoExperimentalCheckOutputFiles.getValue()) {
-      flags.add(NO_CHECK_OUTPUTS);
-    }
     flags.add("--curses=no");
     flags.add("--color=no");
   }
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 5346e8c..6099fc2 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
@@ -31,6 +31,7 @@
   public static final String BUILD_LANGUAGE = "build-language";
   public static final String OUTPUT_BASE_KEY = "output_base";
   public static final String MASTER_LOG = "master-log";
+  public static final String RELEASE = "release";
 
   public static String blazeBinKey(BuildSystem buildSystem) {
     switch (buildSystem) {
diff --git a/base/src/com/google/idea/blaze/base/ide/NewBlazePackageAction.java b/base/src/com/google/idea/blaze/base/ide/NewBlazePackageAction.java
index 3780bf1..2d0f519 100644
--- a/base/src/com/google/idea/blaze/base/ide/NewBlazePackageAction.java
+++ b/base/src/com/google/idea/blaze/base/ide/NewBlazePackageAction.java
@@ -18,7 +18,7 @@
 
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.Lists;
-import com.google.idea.blaze.base.actions.BlazeAction;
+import com.google.idea.blaze.base.actions.BlazeProjectAction;
 import com.google.idea.blaze.base.buildmodifier.BuildFileModifier;
 import com.google.idea.blaze.base.buildmodifier.FileSystemModifier;
 import com.google.idea.blaze.base.metrics.Action;
@@ -41,7 +41,6 @@
 import com.intellij.ide.IdeView;
 import com.intellij.ide.util.DirectoryChooserUtil;
 import com.intellij.openapi.actionSystem.AnActionEvent;
-import com.intellij.openapi.actionSystem.CommonDataKeys;
 import com.intellij.openapi.actionSystem.LangDataKeys;
 import com.intellij.openapi.actionSystem.Presentation;
 import com.intellij.openapi.diagnostic.Logger;
@@ -60,7 +59,7 @@
 import javax.annotation.Nullable;
 import org.jetbrains.annotations.NotNull;
 
-class NewBlazePackageAction extends BlazeAction implements DumbAware {
+class NewBlazePackageAction extends BlazeProjectAction implements DumbAware {
   private static final Logger LOG = Logger.getInstance(NewBlazePackageAction.class);
 
   private static final String BUILD_FILE_NAME = "BUILD";
@@ -70,9 +69,8 @@
   }
 
   @Override
-  public void actionPerformed(AnActionEvent event) {
+  protected void actionPerformedInBlazeProject(Project project, AnActionEvent event) {
     final IdeView view = event.getData(LangDataKeys.IDE_VIEW);
-    final Project project = event.getData(CommonDataKeys.PROJECT);
     Scope.root(
         new ScopedOperation() {
           @Override
@@ -160,10 +158,10 @@
   }
 
   @Override
-  protected void doUpdate(@NotNull AnActionEvent event) {
+  protected void updateForBlazeProject(Project project, @NotNull AnActionEvent event) {
     Presentation presentation = event.getPresentation();
     if (isEnabled(event)) {
-      String text = String.format("New %s Package", Blaze.buildSystemName(event.getProject()));
+      String text = String.format("New %s Package", Blaze.buildSystemName(project));
       presentation.setEnabledAndVisible(true);
       presentation.setText(text);
       presentation.setDescription(text);
diff --git a/base/src/com/google/idea/blaze/base/ide/NewBlazeRuleAction.java b/base/src/com/google/idea/blaze/base/ide/NewBlazeRuleAction.java
index d67dccf..422dada 100644
--- a/base/src/com/google/idea/blaze/base/ide/NewBlazeRuleAction.java
+++ b/base/src/com/google/idea/blaze/base/ide/NewBlazeRuleAction.java
@@ -16,7 +16,7 @@
 
 package com.google.idea.blaze.base.ide;
 
-import com.google.idea.blaze.base.actions.BlazeAction;
+import com.google.idea.blaze.base.actions.BlazeProjectAction;
 import com.google.idea.blaze.base.experiments.ExperimentScope;
 import com.google.idea.blaze.base.metrics.Action;
 import com.google.idea.blaze.base.scope.Scope;
@@ -34,20 +34,15 @@
 import com.intellij.openapi.project.Project;
 import com.intellij.openapi.vfs.VirtualFile;
 import javax.annotation.Nullable;
-import org.jetbrains.annotations.NotNull;
 
-class NewBlazeRuleAction extends BlazeAction implements DumbAware {
+class NewBlazeRuleAction extends BlazeProjectAction implements DumbAware {
 
   public NewBlazeRuleAction() {
     super();
   }
 
   @Override
-  public void actionPerformed(AnActionEvent event) {
-    final Project project = event.getData(CommonDataKeys.PROJECT);
-    if (project == null) {
-      return;
-    }
+  protected void actionPerformedInBlazeProject(Project project, AnActionEvent event) {
     final VirtualFile virtualFile = event.getData(CommonDataKeys.VIRTUAL_FILE);
     if (virtualFile == null) {
       return;
@@ -68,15 +63,12 @@
   }
 
   @Override
-  protected void doUpdate(@NotNull AnActionEvent event) {
+  protected void updateForBlazeProject(Project project, AnActionEvent event) {
     Presentation presentation = event.getPresentation();
     DataContext dataContext = event.getDataContext();
     VirtualFile file = CommonDataKeys.VIRTUAL_FILE.getData(dataContext);
-    Project project = CommonDataKeys.PROJECT.getData(dataContext);
     boolean enabled =
-        (project != null
-            && file != null
-            && Blaze.getBuildSystemProvider(project).isBuildFile(file.getName()));
+        file != null && Blaze.getBuildSystemProvider(project).isBuildFile(file.getName());
     presentation.setVisible(enabled || ActionPlaces.isMainMenuOrActionSearch(event.getPlace()));
     presentation.setEnabled(enabled);
     presentation.setText(getText(project));
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 0a347c6..d1e99a1 100644
--- a/base/src/com/google/idea/blaze/base/ideinfo/TargetKey.java
+++ b/base/src/com/google/idea/blaze/base/ideinfo/TargetKey.java
@@ -15,36 +15,42 @@
  */
 package com.google.idea.blaze.base.ideinfo;
 
+import com.google.common.base.Joiner;
 import com.google.common.base.Objects;
 import com.google.common.collect.ComparisonChain;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Ordering;
 import com.google.idea.blaze.base.model.primitives.Label;
 import java.io.Serializable;
-import javax.annotation.Nullable;
+import java.util.List;
 
 /** A key that uniquely idenfifies a target in the target map */
 public class TargetKey implements Serializable, Comparable<TargetKey> {
-  private static final long serialVersionUID = 2L;
+  private static final long serialVersionUID = 3L;
 
   public final Label label;
-  @Nullable private final String aspectId;
+  private final ImmutableList<String> aspectIds;
 
-  private TargetKey(Label label, @Nullable String aspectId) {
+  private TargetKey(Label label, ImmutableList<String> aspectIds) {
     this.label = label;
-    this.aspectId = aspectId;
+    this.aspectIds = aspectIds;
   }
 
   /** Returns a key identifying a plain target */
   public static TargetKey forPlainTarget(Label label) {
-    return new TargetKey(label, null);
+    return new TargetKey(label, ImmutableList.of());
   }
 
   /** Returns a key identifying a general target */
-  public static TargetKey forGeneralTarget(Label label, @Nullable String aspectId) {
-    return new TargetKey(label, aspectId);
+  public static TargetKey forGeneralTarget(Label label, List<String> aspectIds) {
+    if (aspectIds.isEmpty()) {
+      return forPlainTarget(label);
+    }
+    return new TargetKey(label, ImmutableList.copyOf(aspectIds));
   }
 
   public boolean isPlainTarget() {
-    return aspectId == null;
+    return aspectIds.isEmpty();
   }
 
   @Override
@@ -56,24 +62,27 @@
       return false;
     }
     TargetKey key = (TargetKey) o;
-    return Objects.equal(label, key.label) && Objects.equal(aspectId, key.aspectId);
+    return Objects.equal(label, key.label) && Objects.equal(aspectIds, key.aspectIds);
   }
 
   @Override
   public int hashCode() {
-    return Objects.hashCode(label, aspectId);
+    return Objects.hashCode(label, aspectIds);
   }
 
   @Override
   public String toString() {
-    if (aspectId == null) {
+    if (aspectIds.isEmpty()) {
       return label.toString();
     }
-    return label.toString() + "#" + aspectId;
+    return label.toString() + "#" + Joiner.on('#').join(aspectIds);
   }
 
   @Override
   public int compareTo(TargetKey o) {
-    return ComparisonChain.start().compare(label, o.label).compare(aspectId, o.aspectId).result();
+    return ComparisonChain.start()
+        .compare(label, o.label)
+        .compare(aspectIds, o.aspectIds, Ordering.natural().lexicographical())
+        .result();
   }
 }
diff --git a/base/src/com/google/idea/blaze/base/lang/buildfile/language/semantics/BuildLanguageSpecProviderImpl.java b/base/src/com/google/idea/blaze/base/lang/buildfile/language/semantics/BuildLanguageSpecProviderImpl.java
index c57a138..f443957 100644
--- a/base/src/com/google/idea/blaze/base/lang/buildfile/language/semantics/BuildLanguageSpecProviderImpl.java
+++ b/base/src/com/google/idea/blaze/base/lang/buildfile/language/semantics/BuildLanguageSpecProviderImpl.java
@@ -15,41 +15,25 @@
  */
 package com.google.idea.blaze.base.lang.buildfile.language.semantics;
 
-import com.google.common.collect.Maps;
 import com.google.idea.blaze.base.lang.buildfile.sync.LanguageSpecResult;
 import com.google.idea.blaze.base.model.BlazeProjectData;
-import com.google.idea.blaze.base.projectview.ProjectViewSet;
-import com.google.idea.blaze.base.scope.BlazeContext;
-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.base.sync.data.BlazeProjectDataManager;
 import com.intellij.openapi.project.Project;
-import java.util.Map;
 
 /** Calls 'blaze info build-language', to retrieve the language spec. */
-public class BuildLanguageSpecProviderImpl extends SyncListener.Adapter
-    implements BuildLanguageSpecProvider {
-
-  private static final Map<Project, LanguageSpecResult> calculatedSpecs = Maps.newHashMap();
+public class BuildLanguageSpecProviderImpl implements BuildLanguageSpecProvider {
 
   @Override
   public BuildLanguageSpec getLanguageSpec(Project project) {
-    LanguageSpecResult result = calculatedSpecs.get(project);
-    return result != null ? result.spec : null;
-  }
-
-  @Override
-  public void onSyncComplete(
-      Project project,
-      BlazeContext context,
-      BlazeImportSettings importSettings,
-      ProjectViewSet projectViewSet,
-      BlazeProjectData blazeProjectData,
-      SyncMode syncMode,
-      SyncResult syncResult) {
-    LanguageSpecResult spec = blazeProjectData.syncState.get(LanguageSpecResult.class);
-    if (spec != null) {
-      calculatedSpecs.put(project, spec);
+    BlazeProjectData blazeProjectData =
+        BlazeProjectDataManager.getInstance(project).getBlazeProjectData();
+    if (blazeProjectData == null) {
+      return null;
     }
+    LanguageSpecResult spec = blazeProjectData.syncState.get(LanguageSpecResult.class);
+    if (spec == null) {
+      return null;
+    }
+    return spec.spec;
   }
 }
diff --git a/base/src/com/google/idea/blaze/base/model/BlazeProjectData.java b/base/src/com/google/idea/blaze/base/model/BlazeProjectData.java
index 84f6936..4a401c8 100644
--- a/base/src/com/google/idea/blaze/base/model/BlazeProjectData.java
+++ b/base/src/com/google/idea/blaze/base/model/BlazeProjectData.java
@@ -22,51 +22,46 @@
 import com.google.idea.blaze.base.sync.projectview.WorkspaceLanguageSettings;
 import com.google.idea.blaze.base.sync.workspace.ArtifactLocationDecoder;
 import com.google.idea.blaze.base.sync.workspace.BlazeRoots;
-import com.google.idea.blaze.base.sync.workspace.WorkingSet;
 import com.google.idea.blaze.base.sync.workspace.WorkspacePathResolver;
 import java.io.Serializable;
-import javax.annotation.Nullable;
 import javax.annotation.concurrent.Immutable;
 
 /** The top-level object serialized to cache. */
 @Immutable
 public class BlazeProjectData implements Serializable {
-  private static final long serialVersionUID = 25L;
+  private static final long serialVersionUID = 27L;
 
   public final long syncTime;
   public final TargetMap targetMap;
   public final ImmutableMap<String, String> blazeInfo;
   public final BlazeRoots blazeRoots;
-  @Nullable public final WorkingSet workingSet;
+  public final BlazeVersionData blazeVersionData;
   public final WorkspacePathResolver workspacePathResolver;
   public final ArtifactLocationDecoder artifactLocationDecoder;
   public final WorkspaceLanguageSettings workspaceLanguageSettings;
   public final SyncState syncState;
   public final ImmutableMultimap<TargetKey, TargetKey> reverseDependencies;
-  @Nullable public final String vcsName;
 
   public BlazeProjectData(
       long syncTime,
       TargetMap targetMap,
       ImmutableMap<String, String> blazeInfo,
       BlazeRoots blazeRoots,
-      @Nullable WorkingSet workingSet,
+      BlazeVersionData blazeVersionData,
       WorkspacePathResolver workspacePathResolver,
       ArtifactLocationDecoder artifactLocationDecoder,
-      WorkspaceLanguageSettings workspaceLangaugeSettings,
+      WorkspaceLanguageSettings workspaceLanguageSettings,
       SyncState syncState,
-      ImmutableMultimap<TargetKey, TargetKey> reverseDependencies,
-      String vcsName) {
+      ImmutableMultimap<TargetKey, TargetKey> reverseDependencies) {
     this.syncTime = syncTime;
     this.targetMap = targetMap;
     this.blazeInfo = blazeInfo;
     this.blazeRoots = blazeRoots;
-    this.workingSet = workingSet;
+    this.blazeVersionData = blazeVersionData;
     this.workspacePathResolver = workspacePathResolver;
     this.artifactLocationDecoder = artifactLocationDecoder;
-    this.workspaceLanguageSettings = workspaceLangaugeSettings;
+    this.workspaceLanguageSettings = workspaceLanguageSettings;
     this.syncState = syncState;
     this.reverseDependencies = reverseDependencies;
-    this.vcsName = vcsName;
   }
 }
diff --git a/base/src/com/google/idea/blaze/base/model/BlazeVersionData.java b/base/src/com/google/idea/blaze/base/model/BlazeVersionData.java
new file mode 100644
index 0000000..1cbdbff
--- /dev/null
+++ b/base/src/com/google/idea/blaze/base/model/BlazeVersionData.java
@@ -0,0 +1,98 @@
+/*
+ * 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.model;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.collect.ImmutableMap;
+import com.google.idea.blaze.base.bazel.BazelVersion;
+import com.google.idea.blaze.base.bazel.BuildSystemProvider;
+import com.google.idea.blaze.base.model.primitives.WorkspaceRoot;
+import com.google.idea.blaze.base.settings.Blaze.BuildSystem;
+import java.io.Serializable;
+import javax.annotation.Nullable;
+
+/**
+ * Version data about the user's blaze/bazel and other info needed for switching behaviour
+ * dynamically.
+ */
+public class BlazeVersionData implements Serializable {
+  private static final long serialVersionUID = 2L;
+
+  @Nullable private final Long blazeCl;
+  @Nullable private final Long clientCl;
+  @Nullable private final BazelVersion bazelVersion;
+
+  @VisibleForTesting
+  public BlazeVersionData() {
+    this(null, null, null);
+  }
+
+  private BlazeVersionData(
+      @Nullable Long blazeCl, @Nullable Long clientCl, @Nullable BazelVersion bazelVersion) {
+    this.blazeCl = blazeCl;
+    this.clientCl = clientCl;
+    this.bazelVersion = bazelVersion;
+  }
+
+  public boolean blazeContainsCl(long cl) {
+    return blazeCl != null && blazeCl >= cl;
+  }
+
+  public boolean blazeClientIsAtLeastCl(long cl) {
+    return clientCl != null && clientCl >= cl;
+  }
+
+  public boolean bazelIsAtLeastVersion(int major, int minor, int bugfix) {
+    return bazelVersion != null && bazelVersion.isAtLeast(major, minor, bugfix);
+  }
+
+  public static BlazeVersionData build(
+      BuildSystem buildSystem,
+      WorkspaceRoot workspaceRoot,
+      ImmutableMap<String, String> blazeInfo) {
+    Builder builder = new Builder();
+    for (BuildSystemProvider provider : BuildSystemProvider.EP_NAME.getExtensions()) {
+      provider.populateBlazeVersionData(buildSystem, workspaceRoot, blazeInfo, builder);
+    }
+    return builder.build();
+  }
+
+  /** Builder class for constructing the blaze version data */
+  public static class Builder {
+    public Long blazeCl;
+    public Long clientCl;
+    public BazelVersion bazelVersion;
+
+    public Builder setBlazeCl(Long blazeCl) {
+      this.blazeCl = blazeCl;
+      return this;
+    }
+
+    public Builder setClientCl(Long clientCl) {
+      this.clientCl = clientCl;
+      return this;
+    }
+
+    public Builder setBazelVersion(BazelVersion bazelVersion) {
+      this.bazelVersion = bazelVersion;
+      return this;
+    }
+
+    public BlazeVersionData build() {
+      return new BlazeVersionData(blazeCl, clientCl, bazelVersion);
+    }
+  }
+}
diff --git a/base/src/com/google/idea/blaze/base/model/primitives/Kind.java b/base/src/com/google/idea/blaze/base/model/primitives/Kind.java
index 88d5199..206a73e 100644
--- a/base/src/com/google/idea/blaze/base/model/primitives/Kind.java
+++ b/base/src/com/google/idea/blaze/base/model/primitives/Kind.java
@@ -49,6 +49,13 @@
   PY_TEST("py_test", LanguageClass.PYTHON),
   PY_APPENGINE_BINARY("py_appengine_binary", LanguageClass.PYTHON),
   PY_WRAP_CC("py_wrap_cc", LanguageClass.PYTHON),
+  GO_TEST("go_test", LanguageClass.GO),
+  GO_APPENGINE_TEST("go_appengine_test", LanguageClass.GO),
+  GO_BINARY("go_binary", LanguageClass.GO),
+  GO_APPENGINE_BINARY("go_appengine_binary", LanguageClass.GO),
+  GO_LIBRARY("go_library", LanguageClass.GO),
+  GO_APPENGINE_LIBRARY("go_appengine_library", LanguageClass.GO),
+  GO_WRAP_CC("go_wrap_cc", LanguageClass.GO),
   ;
 
   static final ImmutableMap<String, Kind> STRING_TO_KIND = makeStringToKindMap();
diff --git a/base/src/com/google/idea/blaze/base/model/primitives/LanguageClass.java b/base/src/com/google/idea/blaze/base/model/primitives/LanguageClass.java
index 9addd48..cf030a2 100644
--- a/base/src/com/google/idea/blaze/base/model/primitives/LanguageClass.java
+++ b/base/src/com/google/idea/blaze/base/model/primitives/LanguageClass.java
@@ -28,6 +28,7 @@
   JAVASCRIPT("javascript", ImmutableSet.of("js", "applejs")),
   TYPESCRIPT("typescript", ImmutableSet.of("ts", "ats")),
   DART("dart", ImmutableSet.of("dart")),
+  GO("go", ImmutableSet.of("go")),
   PYTHON("python", ImmutableSet.of("py", "pyw"));
 
   private static final ImmutableMap<String, LanguageClass> RECOGNIZED_EXTENSIONS =
diff --git a/base/src/com/google/idea/blaze/base/model/primitives/WorkspaceType.java b/base/src/com/google/idea/blaze/base/model/primitives/WorkspaceType.java
index 290118b..c88ad5a 100644
--- a/base/src/com/google/idea/blaze/base/model/primitives/WorkspaceType.java
+++ b/base/src/com/google/idea/blaze/base/model/primitives/WorkspaceType.java
@@ -15,6 +15,8 @@
  */
 package com.google.idea.blaze.base.model.primitives;
 
+import com.google.common.collect.ImmutableSet;
+
 /**
  * Workspace types.
  *
@@ -27,22 +29,23 @@
   JAVA("java", LanguageClass.JAVA),
   PYTHON("python", LanguageClass.PYTHON),
   JAVASCRIPT("javascript", LanguageClass.JAVASCRIPT),
+  GO("go", LanguageClass.GO),
   INTELLIJ_PLUGIN("intellij_plugin", LanguageClass.JAVA);
 
   private final String name;
   // the languages active by default for this WorkspaceType
-  private final LanguageClass[] languages;
+  private final ImmutableSet<LanguageClass> languages;
 
   WorkspaceType(String name, LanguageClass... languages) {
     this.name = name;
-    this.languages = languages;
+    this.languages = ImmutableSet.copyOf(languages);
   }
 
   public String getName() {
     return name;
   }
 
-  public LanguageClass[] getLanguages() {
+  public ImmutableSet<LanguageClass> getLanguages() {
     return languages;
   }
 
diff --git a/base/src/com/google/idea/blaze/base/projectview/ProjectViewVerifier.java b/base/src/com/google/idea/blaze/base/projectview/ProjectViewVerifier.java
index 553900d..0953d07 100644
--- a/base/src/com/google/idea/blaze/base/projectview/ProjectViewVerifier.java
+++ b/base/src/com/google/idea/blaze/base/projectview/ProjectViewVerifier.java
@@ -18,7 +18,6 @@
 import com.google.common.collect.Lists;
 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.projectview.section.ListSection;
 import com.google.idea.blaze.base.projectview.section.sections.DirectoryEntry;
 import com.google.idea.blaze.base.projectview.section.sections.DirectorySection;
@@ -27,7 +26,9 @@
 import com.google.idea.blaze.base.scope.output.IssueOutput;
 import com.google.idea.blaze.base.sync.BlazeSyncPlugin;
 import com.google.idea.blaze.base.sync.projectview.WorkspaceLanguageSettings;
+import com.google.idea.blaze.base.sync.workspace.WorkspacePathResolver;
 import com.intellij.openapi.util.io.FileUtil;
+import java.io.File;
 import java.util.List;
 
 /** Verifies project views. */
@@ -44,16 +45,7 @@
   /** Verifies the project view. Any errors are output to the context as issues. */
   public static boolean verifyProjectView(
       BlazeContext context,
-      WorkspaceRoot workspaceRoot,
-      ProjectViewSet projectViewSet,
-      WorkspaceLanguageSettings workspaceLanguageSettings) {
-    return verifyProjectViewNoDisk(context, projectViewSet, workspaceLanguageSettings)
-        && verifyIncludedPackagesExistOnDisk(context, workspaceRoot, projectViewSet);
-  }
-
-  /** Verifies the project view, without hitting disk. */
-  public static boolean verifyProjectViewNoDisk(
-      BlazeContext context,
+      WorkspacePathResolver workspacePathResolver,
       ProjectViewSet projectViewSet,
       WorkspaceLanguageSettings workspaceLanguageSettings) {
     if (!verifyIncludedPackagesAreNotExcluded(context, projectViewSet)) {
@@ -69,6 +61,9 @@
           .inFile(projectViewSet.getTopLevelProjectViewFile().projectViewFile)
           .submit(context);
     }
+    if (!verifyIncludedPackagesExistOnDisk(context, workspacePathResolver, projectViewSet)) {
+      return false;
+    }
     return true;
   }
 
@@ -119,7 +114,9 @@
   }
 
   private static boolean verifyIncludedPackagesExistOnDisk(
-      BlazeContext context, WorkspaceRoot workspaceRoot, ProjectViewSet projectViewSet) {
+      BlazeContext context,
+      WorkspacePathResolver workspacePathResolver,
+      ProjectViewSet projectViewSet) {
     boolean ok = true;
 
     FileAttributeProvider fileAttributeProvider = FileAttributeProvider.getInstance();
@@ -135,12 +132,19 @@
           continue;
         }
         WorkspacePath workspacePath = entry.directory;
-        if (!fileAttributeProvider.exists(workspaceRoot.fileForPath(workspacePath))) {
+        File file = workspacePathResolver.resolveToFile(workspacePath);
+        if (!fileAttributeProvider.exists(file)) {
           IssueOutput.error(
                   String.format(
-                      "Directory '%s' specified in import roots not found "
-                          + "under workspace root '%s'",
-                      workspacePath, workspaceRoot))
+                      "Directory '%s' specified in project view not found.", workspacePath))
+              .inFile(projectViewFile.projectViewFile)
+              .withData(new MissingDirectoryIssueData(workspacePath))
+              .submit(context);
+          ok = false;
+        } else if (!fileAttributeProvider.isDirectory(file)) {
+          IssueOutput.error(
+                  String.format(
+                      "Directory '%s' specified in project view is a file.", workspacePath))
               .inFile(projectViewFile.projectViewFile)
               .withData(new MissingDirectoryIssueData(workspacePath))
               .submit(context);
diff --git a/base/src/com/google/idea/blaze/base/projectview/section/sections/RunConfigurationsSection.java b/base/src/com/google/idea/blaze/base/projectview/section/sections/RunConfigurationsSection.java
new file mode 100644
index 0000000..9ffda84
--- /dev/null
+++ b/base/src/com/google/idea/blaze/base/projectview/section/sections/RunConfigurationsSection.java
@@ -0,0 +1,63 @@
+/*
+ * 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.projectview.section.sections;
+
+import com.google.common.collect.Lists;
+import com.google.idea.blaze.base.model.primitives.WorkspacePath;
+import com.google.idea.blaze.base.projectview.parser.ParseContext;
+import com.google.idea.blaze.base.projectview.parser.ProjectViewParser;
+import com.google.idea.blaze.base.projectview.section.ListSection;
+import com.google.idea.blaze.base.projectview.section.ListSectionParser;
+import com.google.idea.blaze.base.projectview.section.SectionKey;
+import com.google.idea.blaze.base.projectview.section.SectionParser;
+import com.google.idea.blaze.base.ui.BlazeValidationError;
+import java.util.List;
+import javax.annotation.Nullable;
+
+/** Allows users to import run configurations from XML files in their workspace. */
+public class RunConfigurationsSection {
+  public static final SectionKey<WorkspacePath, ListSection<WorkspacePath>> KEY =
+      SectionKey.of("import_run_configurations");
+  public static final SectionParser PARSER = new RunConfigurationsSectionParser();
+
+  private static class RunConfigurationsSectionParser extends ListSectionParser<WorkspacePath> {
+    private RunConfigurationsSectionParser() {
+      super(KEY);
+    }
+
+    @Nullable
+    @Override
+    protected WorkspacePath parseItem(ProjectViewParser parser, ParseContext parseContext) {
+      String text = parseContext.current().text;
+      List<BlazeValidationError> errors = Lists.newArrayList();
+      if (!WorkspacePath.validate(text, errors)) {
+        parseContext.addErrors(errors);
+        return null;
+      }
+      return new WorkspacePath(text);
+    }
+
+    @Override
+    protected void printItem(WorkspacePath item, StringBuilder sb) {
+      sb.append(item);
+    }
+
+    @Override
+    public ItemType getItemType() {
+      return ItemType.FileSystemItem;
+    }
+  }
+}
diff --git a/base/src/com/google/idea/blaze/base/projectview/section/sections/Sections.java b/base/src/com/google/idea/blaze/base/projectview/section/sections/Sections.java
index 93b2fd9..ad0c35e 100644
--- a/base/src/com/google/idea/blaze/base/projectview/section/sections/Sections.java
+++ b/base/src/com/google/idea/blaze/base/projectview/section/sections/Sections.java
@@ -36,7 +36,8 @@
           ImportTargetOutputSection.PARSER,
           ExcludeTargetSection.PARSER,
           ExcludedSourceSection.PARSER,
-          MetricsProjectSection.PARSER);
+          MetricsProjectSection.PARSER,
+          RunConfigurationsSection.PARSER);
 
   public static List<SectionParser> getParsers() {
     List<SectionParser> parsers = Lists.newArrayList(PARSERS);
diff --git a/base/src/com/google/idea/blaze/base/run/BlazeBeforeRunTaskProvider.java b/base/src/com/google/idea/blaze/base/run/BlazeBeforeRunTaskProvider.java
index 7eb51e3..9b5e0e1 100644
--- a/base/src/com/google/idea/blaze/base/run/BlazeBeforeRunTaskProvider.java
+++ b/base/src/com/google/idea/blaze/base/run/BlazeBeforeRunTaskProvider.java
@@ -20,6 +20,7 @@
 import com.intellij.execution.BeforeRunTask;
 import com.intellij.execution.BeforeRunTaskProvider;
 import com.intellij.execution.configurations.RunConfiguration;
+import com.intellij.execution.configurations.WrappingRunConfiguration;
 import com.intellij.execution.runners.ExecutionEnvironment;
 import com.intellij.openapi.actionSystem.DataContext;
 import com.intellij.openapi.util.Key;
@@ -91,6 +92,9 @@
 
   @Override
   public boolean canExecuteTask(RunConfiguration configuration, Task task) {
+    if (configuration instanceof WrappingRunConfiguration) {
+      configuration = ((WrappingRunConfiguration) configuration).getPeer();
+    }
     return configuration instanceof BlazeCommandRunConfiguration;
   }
 
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 d6200ee..07fd7e3 100644
--- a/base/src/com/google/idea/blaze/base/run/BlazeCommandRunConfiguration.java
+++ b/base/src/com/google/idea/blaze/base/run/BlazeCommandRunConfiguration.java
@@ -36,18 +36,21 @@
 import com.intellij.execution.RunnerIconProvider;
 import com.intellij.execution.configurations.ConfigurationFactory;
 import com.intellij.execution.configurations.LocatableConfigurationBase;
+import com.intellij.execution.configurations.ModuleRunProfile;
 import com.intellij.execution.configurations.RunConfiguration;
 import com.intellij.execution.configurations.RunProfileState;
 import com.intellij.execution.configurations.RuntimeConfigurationError;
 import com.intellij.execution.configurations.RuntimeConfigurationException;
 import com.intellij.execution.runners.ExecutionEnvironment;
 import com.intellij.openapi.diagnostic.Logger;
+import com.intellij.openapi.module.Module;
 import com.intellij.openapi.options.SettingsEditor;
 import com.intellij.openapi.project.Project;
 import com.intellij.openapi.util.InvalidDataException;
 import com.intellij.openapi.util.WriteExternalException;
 import com.intellij.ui.TextFieldWithAutoCompletion;
 import com.intellij.ui.TextFieldWithAutoCompletion.StringsCompletionProvider;
+import com.intellij.ui.components.JBCheckBox;
 import com.intellij.ui.components.JBLabel;
 import com.intellij.util.ui.UIUtil;
 import java.util.Collection;
@@ -63,13 +66,15 @@
 
 /** A run configuration which executes Blaze commands. */
 public class BlazeCommandRunConfiguration extends LocatableConfigurationBase
-    implements BlazeRunConfiguration, RunnerIconProvider {
+    implements BlazeRunConfiguration, RunnerIconProvider, ModuleRunProfile {
 
   private static final Logger LOG = Logger.getInstance(BlazeCommandRunConfiguration.class);
 
   private static final String HANDLER_ATTR = "handler-id";
   private static final String TARGET_TAG = "blaze-target";
   private static final String KIND_ATTR = "kind";
+  private static final String KEEP_IN_SYNC_TAG = "keep-in-sync";
+
   /**
    * This tag is actually written by {@link com.intellij.execution.impl.RunManagerImpl}; it
    * represents the before-run tasks of the configuration. We need to know about it to avoid writing
@@ -83,6 +88,10 @@
   @Nullable private TargetExpression target;
   // Null if the target is null, not a Label, or not a known rule.
   @Nullable private Kind targetKind;
+
+  // for keeping imported configurations in sync with their source XML
+  @Nullable private Boolean keepInSync = null;
+
   private BlazeCommandRunConfigurationHandlerProvider handlerProvider;
   private BlazeCommandRunConfigurationHandler handler;
 
@@ -118,6 +127,17 @@
   }
 
   @Override
+  public void setKeepInSync(@Nullable Boolean keepInSync) {
+    this.keepInSync = keepInSync;
+  }
+
+  @Override
+  @Nullable
+  public Boolean getKeepInSync() {
+    return keepInSync;
+  }
+
+  @Override
   @Nullable
   public TargetExpression getTarget() {
     return target;
@@ -125,6 +145,10 @@
 
   public void setTarget(@Nullable TargetExpression target) {
     this.target = target;
+    updateHandler();
+  }
+
+  private void updateHandler() {
     targetKind = getKindForTarget();
 
     BlazeCommandRunConfigurationHandlerProvider handlerProvider =
@@ -208,6 +232,9 @@
     super.readExternal(element);
     element = element.clone();
 
+    String keepInSyncString = element.getAttributeValue(KEEP_IN_SYNC_TAG);
+    keepInSync = keepInSyncString != null ? Boolean.parseBoolean(keepInSyncString) : null;
+
     // Target is persisted as a tag to permit multiple targets in the future.
     Element targetElement = element.getChild(TARGET_TAG);
     if (targetElement != null && !Strings.isNullOrEmpty(targetElement.getTextTrim())) {
@@ -234,6 +261,7 @@
     element.removeAttribute(KIND_ATTR);
     element.removeAttribute(HANDLER_ATTR);
     element.removeChildren(TARGET_TAG);
+    element.removeAttribute(KEEP_IN_SYNC_TAG);
     // remove legacy attribute, if present
     element.removeAttribute(TARGET_TAG);
 
@@ -253,7 +281,11 @@
       }
       element.addContent(targetElement);
     }
+    if (keepInSync != null) {
+      element.setAttribute(KEEP_IN_SYNC_TAG, Boolean.toString(keepInSync));
+    }
     element.setAttribute(HANDLER_ATTR, handlerProvider.getId());
+
     handler.getState().writeExternal(elementState);
 
     // copy our internal state to the provided Element, skipping items already present
@@ -283,6 +315,7 @@
     configuration.elementState = elementState.clone();
     configuration.target = target;
     configuration.targetKind = targetKind;
+    configuration.keepInSync = keepInSync;
     configuration.handlerProvider = handlerProvider;
     configuration.handler = handlerProvider.createHandler(this);
     try {
@@ -298,6 +331,11 @@
   @Nullable
   public RunProfileState getState(Executor executor, ExecutionEnvironment environment)
       throws ExecutionException {
+    if (target != null) {
+      // We need to update the handler manually because it might otherwise be out of date (e.g.
+      // because the target map has changed since the last update).
+      updateHandler();
+    }
     BlazeCommandRunConfigurationRunner runner = handler.createRunner(executor, environment);
     if (runner != null) {
       environment.putCopyableUserData(BlazeCommandRunConfigurationRunner.RUNNER_KEY, runner);
@@ -323,6 +361,11 @@
     return new BlazeCommandRunConfigurationSettingsEditor(this);
   }
 
+  @Override
+  public Module[] getModules() {
+    return new Module[0];
+  }
+
   static class BlazeCommandRunConfigurationSettingsEditor
       extends SettingsEditor<BlazeCommandRunConfiguration> {
 
@@ -332,7 +375,9 @@
     private JComponent handlerStateComponent;
     private Element elementState;
 
+    private final Box editorWithoutSyncCheckBox;
     private final Box editor;
+    private final JBCheckBox keepInSyncCheckBox;
     private final JBLabel targetExpressionLabel;
     private final TextFieldWithAutoCompletion<String> targetField;
 
@@ -343,16 +388,35 @@
               project, new TargetCompletionProvider(project), true, null);
       elementState = config.elementState.clone();
       targetExpressionLabel = new JBLabel(UIUtil.ComponentStyle.LARGE);
-      editor = UiUtil.createBox(targetExpressionLabel, targetField);
-      updateTargetExpressionLabel(config);
+      keepInSyncCheckBox = new JBCheckBox("Keep in sync with source XML");
+      editorWithoutSyncCheckBox = UiUtil.createBox(targetExpressionLabel, targetField);
+      editor = UiUtil.createBox(editorWithoutSyncCheckBox, keepInSyncCheckBox);
+      updateEditor(config);
       updateHandlerEditor(config);
+      keepInSyncCheckBox.addItemListener(e -> updateEnabledStatus());
     }
 
-    private void updateTargetExpressionLabel(BlazeCommandRunConfiguration config) {
+    private void updateEditor(BlazeCommandRunConfiguration config) {
       targetExpressionLabel.setText(
           String.format(
               "Target expression (%s handled by %s):",
               config.getTargetKindName(), config.handler.getHandlerName()));
+      keepInSyncCheckBox.setVisible(config.keepInSync != null);
+      if (config.keepInSync != null) {
+        keepInSyncCheckBox.setSelected(config.keepInSync);
+      }
+      updateEnabledStatus();
+    }
+
+    private void updateEnabledStatus() {
+      setEnabled(!keepInSyncCheckBox.isVisible() || !keepInSyncCheckBox.isSelected());
+    }
+
+    private void setEnabled(boolean enabled) {
+      if (handlerStateEditor != null) {
+        handlerStateEditor.setComponentEnabled(enabled);
+      }
+      targetField.setEnabled(enabled);
     }
 
     private void updateHandlerEditor(BlazeCommandRunConfiguration config) {
@@ -366,10 +430,10 @@
       handlerStateEditor = handler.getState().getEditor(config.getProject());
 
       if (handlerStateComponent != null) {
-        editor.remove(handlerStateComponent);
+        editorWithoutSyncCheckBox.remove(handlerStateComponent);
       }
       handlerStateComponent = handlerStateEditor.createComponent();
-      editor.add(handlerStateComponent);
+      editorWithoutSyncCheckBox.add(handlerStateComponent);
     }
 
     @Override
@@ -380,7 +444,7 @@
     @Override
     protected void resetEditorFrom(BlazeCommandRunConfiguration config) {
       elementState = config.elementState.clone();
-      updateTargetExpressionLabel(config);
+      updateEditor(config);
       if (config.handlerProvider != handlerProvider) {
         updateHandlerEditor(config);
       }
@@ -397,6 +461,7 @@
       } catch (WriteExternalException e) {
         LOG.error(e);
       }
+      config.keepInSync = keepInSyncCheckBox.isVisible() ? keepInSyncCheckBox.isSelected() : null;
 
       // now set the config's state, based on the editor's (possibly out of date) handler
       config.updateHandlerIfDifferentProvider(handlerProvider);
@@ -411,7 +476,7 @@
       String targetString = targetField.getText();
       config.setTarget(
           Strings.isNullOrEmpty(targetString) ? null : TargetExpression.fromString(targetString));
-      updateTargetExpressionLabel(config);
+      updateEditor(config);
       if (config.handlerProvider != handlerProvider) {
         updateHandlerEditor(config);
         handlerStateEditor.resetEditorFrom(config.handler.getState());
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 f3409af..5a4daed 100644
--- a/base/src/com/google/idea/blaze/base/run/BlazeRunConfiguration.java
+++ b/base/src/com/google/idea/blaze/base/run/BlazeRunConfiguration.java
@@ -22,4 +22,9 @@
 public interface BlazeRunConfiguration {
   @Nullable
   TargetExpression getTarget();
+
+  /** Keep in sync with source XML */
+  void setKeepInSync(@Nullable Boolean keepInSync);
+
+  Boolean getKeepInSync();
 }
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 74a3812..a1e0549 100644
--- a/base/src/com/google/idea/blaze/base/run/BlazeRunConfigurationSyncListener.java
+++ b/base/src/com/google/idea/blaze/base/run/BlazeRunConfigurationSyncListener.java
@@ -20,20 +20,28 @@
 import com.google.idea.blaze.base.model.primitives.Label;
 import com.google.idea.blaze.base.model.primitives.TargetExpression;
 import com.google.idea.blaze.base.projectview.ProjectViewSet;
+import com.google.idea.blaze.base.projectview.section.sections.RunConfigurationsSection;
 import com.google.idea.blaze.base.projectview.section.sections.TargetSection;
+import com.google.idea.blaze.base.run.exporter.RunConfigurationSerializer;
 import com.google.idea.blaze.base.scope.BlazeContext;
 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.base.sync.workspace.WorkspacePathResolver;
+import com.google.idea.sdkcompat.transactions.Transactions;
 import com.intellij.execution.RunManager;
 import com.intellij.execution.RunnerAndConfigurationSettings;
 import com.intellij.execution.configurations.RunConfiguration;
 import com.intellij.openapi.project.Project;
-import com.intellij.util.ui.UIUtil;
+import java.io.File;
 import java.util.List;
 import java.util.Set;
+import java.util.stream.Collectors;
 
-/** Creates run configurations for project view targets, where appropriate. */
+/**
+ * Imports run configurations specified in the project view, and creates run configurations for
+ * project view targets, where appropriate.
+ */
 public class BlazeRunConfigurationSyncListener extends SyncListener.Adapter {
 
   @Override
@@ -45,23 +53,40 @@
       BlazeProjectData blazeProjectData,
       SyncMode syncMode,
       SyncResult syncResult) {
+    if (syncMode == SyncMode.STARTUP) {
+      return;
+    }
 
-    UIUtil.invokeAndWaitIfNeeded(
-        (Runnable)
-            () -> {
-              Set<Label> labelsWithConfigs = labelsWithConfigs(project);
-              Set<TargetExpression> targetExpressions =
-                  Sets.newHashSet(projectViewSet.listItems(TargetSection.KEY));
-              // We only auto-generate configurations for rules listed in the project view.
-              for (TargetExpression target : targetExpressions) {
-                if (!(target instanceof Label) || labelsWithConfigs.contains(target)) {
-                  continue;
-                }
-                Label label = (Label) target;
-                labelsWithConfigs.add(label);
-                maybeAddRunConfiguration(project, blazeProjectData, label);
-              }
-            });
+    Set<File> xmlFiles =
+        getImportedRunConfigurations(projectViewSet, blazeProjectData.workspacePathResolver);
+    Transactions.submitTransactionAndWait(
+        () -> {
+          // First, import from specified XML files. Then auto-generate from targets.
+          xmlFiles.forEach(
+              (file) -> RunConfigurationSerializer.loadFromXmlIgnoreExisting(project, file));
+
+          Set<Label> labelsWithConfigs = labelsWithConfigs(project);
+          Set<TargetExpression> targetExpressions =
+              Sets.newHashSet(projectViewSet.listItems(TargetSection.KEY));
+          // We only auto-generate configurations for rules listed in the project view.
+          for (TargetExpression target : targetExpressions) {
+            if (!(target instanceof Label) || labelsWithConfigs.contains(target)) {
+              continue;
+            }
+            Label label = (Label) target;
+            labelsWithConfigs.add(label);
+            maybeAddRunConfiguration(project, blazeProjectData, label);
+          }
+        });
+  }
+
+  private static Set<File> getImportedRunConfigurations(
+      ProjectViewSet projectViewSet, WorkspacePathResolver pathResolver) {
+    return projectViewSet
+        .listItems(RunConfigurationsSection.KEY)
+        .stream()
+        .map(pathResolver::resolveToFile)
+        .collect(Collectors.toSet());
   }
 
   /** Collects a set of all the Blaze labels that have an associated run configuration. */
diff --git a/base/src/com/google/idea/blaze/base/run/DistributedExecutorSupport.java b/base/src/com/google/idea/blaze/base/run/DistributedExecutorSupport.java
new file mode 100644
index 0000000..851eab6
--- /dev/null
+++ b/base/src/com/google/idea/blaze/base/run/DistributedExecutorSupport.java
@@ -0,0 +1,64 @@
+/*
+ * 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.run;
+
+import com.google.common.collect.ImmutableList;
+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.util.List;
+import javax.annotation.Nullable;
+
+/** Information about any distributed executor available to the build system. */
+public interface DistributedExecutorSupport {
+
+  ExtensionPointName<DistributedExecutorSupport> EP_NAME =
+      ExtensionPointName.create("com.google.idea.blaze.DistributedExecutorSupport");
+
+  /**
+   * Returns the name of an available distributed executor, if one exists for the given build
+   * system.
+   */
+  @Nullable
+  static DistributedExecutorSupport getAvailableExecutor(BuildSystem buildSystem) {
+    for (DistributedExecutorSupport executor : EP_NAME.getExtensions()) {
+      if (executor.isAvailable(buildSystem)) {
+        return executor;
+      }
+    }
+    return null;
+  }
+
+  /** Returns the blaze/bazel flags required to specify whether to run on a distributed executor. */
+  static List<String> getBlazeFlags(Project project, @Nullable Boolean runDistributed) {
+    if (runDistributed == null) {
+      return ImmutableList.of();
+    }
+    DistributedExecutorSupport executorInfo = getAvailableExecutor(Blaze.getBuildSystem(project));
+    if (executorInfo == null) {
+      return ImmutableList.of();
+    }
+    return ImmutableList.of(executorInfo.getBlazeFlag(runDistributed));
+  }
+
+  String executorName();
+
+  boolean isAvailable(BuildSystem buildSystem);
+
+  /** Get blaze/bazel flag specifying whether to run on this distributed executor */
+  String getBlazeFlag(boolean runDistributed);
+}
diff --git a/base/src/com/google/idea/blaze/base/run/confighandler/BlazeCommandGenericRunConfigurationHandler.java b/base/src/com/google/idea/blaze/base/run/confighandler/BlazeCommandGenericRunConfigurationHandler.java
index 0080b3f..da3d641 100644
--- a/base/src/com/google/idea/blaze/base/run/confighandler/BlazeCommandGenericRunConfigurationHandler.java
+++ b/base/src/com/google/idea/blaze/base/run/confighandler/BlazeCommandGenericRunConfigurationHandler.java
@@ -20,6 +20,7 @@
 import com.google.idea.blaze.base.run.BlazeConfigurationNameBuilder;
 import com.google.idea.blaze.base.run.state.BlazeCommandRunConfigurationCommonState;
 import com.google.idea.blaze.base.settings.Blaze;
+import com.google.idea.blaze.base.settings.Blaze.BuildSystem;
 import com.intellij.execution.Executor;
 import com.intellij.execution.configurations.RunConfiguration;
 import com.intellij.execution.configurations.RuntimeConfigurationException;
@@ -38,8 +39,9 @@
   private final BlazeCommandRunConfigurationCommonState state;
 
   public BlazeCommandGenericRunConfigurationHandler(BlazeCommandRunConfiguration configuration) {
-    this.buildSystemName = Blaze.buildSystemName(configuration.getProject());
-    this.state = new BlazeCommandRunConfigurationCommonState(buildSystemName);
+    BuildSystem buildSystem = Blaze.getBuildSystem(configuration.getProject());
+    this.buildSystemName = buildSystem.getName();
+    this.state = new BlazeCommandRunConfigurationCommonState(buildSystem);
   }
 
   @Override
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 79a5147..d2c6512 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
@@ -18,6 +18,7 @@
 import com.google.common.collect.ImmutableList;
 import com.google.idea.blaze.base.async.process.LineProcessingOutputStream;
 import com.google.idea.blaze.base.command.BlazeCommand;
+import com.google.idea.blaze.base.command.BlazeCommandName;
 import com.google.idea.blaze.base.command.BlazeFlags;
 import com.google.idea.blaze.base.issueparser.IssueOutputLineProcessor;
 import com.google.idea.blaze.base.metrics.Action;
@@ -25,8 +26,12 @@
 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.BlazeTestEventsHandlerProvider;
+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.IdeaLogScope;
@@ -35,15 +40,22 @@
 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.intellij.execution.DefaultExecutionResult;
 import com.intellij.execution.ExecutionException;
+import com.intellij.execution.ExecutionResult;
 import com.intellij.execution.Executor;
 import com.intellij.execution.configurations.CommandLineState;
 import com.intellij.execution.configurations.RunProfile;
 import com.intellij.execution.configurations.RunProfileState;
+import com.intellij.execution.configurations.WrappingRunConfiguration;
+import com.intellij.execution.filters.TextConsoleBuilderImpl;
 import com.intellij.execution.process.ProcessHandler;
 import com.intellij.execution.process.ProcessListener;
 import com.intellij.execution.runners.ExecutionEnvironment;
+import com.intellij.execution.runners.ProgramRunner;
+import com.intellij.execution.ui.ConsoleView;
 import com.intellij.openapi.project.Project;
+import javax.annotation.Nullable;
 import org.jetbrains.annotations.NotNull;
 
 /**
@@ -55,7 +67,7 @@
 
   @Override
   public RunProfileState getRunProfileState(Executor executor, ExecutionEnvironment environment) {
-    return new BlazeCommandRunProfileState(environment);
+    return new BlazeCommandRunProfileState(environment, null);
   }
 
   @Override
@@ -68,13 +80,31 @@
   public static class BlazeCommandRunProfileState extends CommandLineState {
     private final BlazeCommandRunConfiguration configuration;
     private final BlazeCommandRunConfigurationCommonState handlerState;
+    @Nullable private final BlazeTestEventsHandlerProvider testEventsHandlerProvider;
 
-    public BlazeCommandRunProfileState(ExecutionEnvironment environment) {
+    public BlazeCommandRunProfileState(
+        ExecutionEnvironment environment,
+        @Nullable BlazeTestEventsHandlerProvider testEventsHandlerProvider) {
       super(environment);
-      RunProfile runProfile = environment.getRunProfile();
-      configuration = (BlazeCommandRunConfiguration) runProfile;
-      handlerState =
+      this.configuration = getConfiguration(environment);
+      this.handlerState =
           (BlazeCommandRunConfigurationCommonState) configuration.getHandler().getState();
+      this.testEventsHandlerProvider = testEventsHandlerProvider;
+    }
+
+    private static BlazeCommandRunConfiguration getConfiguration(ExecutionEnvironment environment) {
+      RunProfile runProfile = environment.getRunProfile();
+      if (runProfile instanceof WrappingRunConfiguration) {
+        runProfile = ((WrappingRunConfiguration) runProfile).getPeer();
+      }
+      return (BlazeCommandRunConfiguration) runProfile;
+    }
+
+    @Override
+    public ExecutionResult execute(Executor executor, ProgramRunner runner)
+        throws ExecutionException {
+      DefaultExecutionResult result = (DefaultExecutionResult) super.execute(executor, runner);
+      return SmRunnerUtils.attachRerunFailedTestsAction(result);
     }
 
     @Override
@@ -88,14 +118,24 @@
       ProjectViewSet projectViewSet = ProjectViewManager.getInstance(project).getProjectViewSet();
       assert projectViewSet != null;
 
-      BlazeCommand blazeCommand =
-          BlazeCommand.builder(Blaze.getBuildSystem(project), handlerState.getCommand())
-              .setBlazeBinary(handlerState.getBlazeBinary())
-              .addTargets(configuration.getTarget())
-              .addBlazeFlags(BlazeFlags.buildFlags(project, projectViewSet))
-              .addBlazeFlags(handlerState.getBlazeFlags())
-              .addExeFlags(handlerState.getExeFlags())
-              .build();
+      ImmutableList<String> testHandlerFlags = ImmutableList.of();
+      BlazeTestEventsHandler testEventsHandler =
+          canUseTestUi() && testEventsHandlerProvider != null
+              ? testEventsHandlerProvider.getHandler()
+              : null;
+      if (testEventsHandler != null) {
+        testHandlerFlags = testEventsHandler.getBlazeFlags();
+        setConsoleBuilder(
+            new TextConsoleBuilderImpl(project) {
+              @Override
+              protected ConsoleView createConsole() {
+                return SmRunnerUtils.getConsoleView(
+                    project, configuration, getEnvironment().getExecutor(), testEventsHandler);
+              }
+            });
+      }
+
+      BlazeCommand blazeCommand = getBlazeCommand(project, testHandlerFlags);
 
       WorkspaceRoot workspaceRoot = WorkspaceRoot.fromImportSettings(importSettings);
       return new ScopedBlazeProcessHandler(
@@ -120,5 +160,28 @@
             }
           });
     }
+
+    private BlazeCommand getBlazeCommand(Project project, ImmutableList<String> testHandlerFlags) {
+      ProjectViewSet projectViewSet = ProjectViewManager.getInstance(project).getProjectViewSet();
+      assert projectViewSet != null;
+
+      return BlazeCommand.builder(Blaze.getBuildSystem(project), handlerState.getCommand())
+          .setBlazeBinary(handlerState.getBlazeBinary())
+          .addTargets(configuration.getTarget())
+          .addBlazeFlags(BlazeFlags.buildFlags(project, projectViewSet))
+          .addBlazeFlags(testHandlerFlags)
+          .addBlazeFlags(handlerState.getBlazeFlags())
+          .addBlazeFlags(
+              DistributedExecutorSupport.getBlazeFlags(
+                  project, handlerState.getRunOnDistributedExecutor()))
+          .addExeFlags(handlerState.getExeFlags())
+          .build();
+    }
+
+    private boolean canUseTestUi() {
+      return testEventsHandlerProvider != null
+          && BlazeCommandName.TEST.equals(handlerState.getCommand())
+          && !Boolean.TRUE.equals(handlerState.getRunOnDistributedExecutor());
+    }
   }
 }
diff --git a/base/src/com/google/idea/blaze/base/run/exporter/ExportRunConfigurationAction.java b/base/src/com/google/idea/blaze/base/run/exporter/ExportRunConfigurationAction.java
new file mode 100644
index 0000000..bc97225
--- /dev/null
+++ b/base/src/com/google/idea/blaze/base/run/exporter/ExportRunConfigurationAction.java
@@ -0,0 +1,46 @@
+/*
+ * 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.run.exporter;
+
+import com.google.idea.blaze.base.actions.BlazeProjectAction;
+import com.google.idea.blaze.base.run.BlazeRunConfiguration;
+import com.intellij.execution.RunManager;
+import com.intellij.openapi.actionSystem.AnActionEvent;
+import com.intellij.openapi.project.DumbAware;
+import com.intellij.openapi.project.Project;
+
+/**
+ * Export selected run configurations to file, so they can be checked in and shared between users.
+ */
+public class ExportRunConfigurationAction extends BlazeProjectAction implements DumbAware {
+
+  @Override
+  protected void updateForBlazeProject(Project project, AnActionEvent e) {
+    boolean hasBlazeConfigs =
+        RunManager.getInstance(project)
+            .getAllConfigurationsList()
+            .stream()
+            .anyMatch((config) -> config instanceof BlazeRunConfiguration);
+    if (!hasBlazeConfigs) {
+      e.getPresentation().setEnabled(false);
+    }
+  }
+
+  @Override
+  protected void actionPerformedInBlazeProject(Project project, AnActionEvent e) {
+    new ExportRunConfigurationDialog(project).show();
+  }
+}
diff --git a/base/src/com/google/idea/blaze/base/run/exporter/ExportRunConfigurationDialog.java b/base/src/com/google/idea/blaze/base/run/exporter/ExportRunConfigurationDialog.java
new file mode 100644
index 0000000..c4802c1
--- /dev/null
+++ b/base/src/com/google/idea/blaze/base/run/exporter/ExportRunConfigurationDialog.java
@@ -0,0 +1,270 @@
+/*
+ * 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.run.exporter;
+
+import com.google.common.base.Strings;
+import com.google.common.collect.ImmutableList;
+import com.google.idea.blaze.base.io.FileAttributeProvider;
+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.projectview.ProjectViewSet.ProjectViewFile;
+import com.google.idea.blaze.base.run.BlazeRunConfiguration;
+import com.google.idea.blaze.base.settings.Blaze;
+import com.intellij.execution.RunManager;
+import com.intellij.execution.configurations.RunConfiguration;
+import com.intellij.icons.AllIcons;
+import com.intellij.openapi.actionSystem.AnActionEvent;
+import com.intellij.openapi.actionSystem.Presentation;
+import com.intellij.openapi.fileChooser.FileChooserDescriptor;
+import com.intellij.openapi.fileChooser.FileChooserDescriptorFactory;
+import com.intellij.openapi.fileChooser.FileChooserDialog;
+import com.intellij.openapi.fileChooser.FileChooserFactory;
+import com.intellij.openapi.project.Project;
+import com.intellij.openapi.ui.DialogWrapper;
+import com.intellij.openapi.ui.ValidationInfo;
+import com.intellij.openapi.util.io.FileUtil;
+import com.intellij.openapi.vfs.LocalFileSystem;
+import com.intellij.openapi.vfs.VirtualFile;
+import com.intellij.ui.AnActionButton;
+import com.intellij.ui.BooleanTableCellEditor;
+import com.intellij.ui.BooleanTableCellRenderer;
+import com.intellij.ui.ColoredTableCellRenderer;
+import com.intellij.ui.FieldPanel;
+import com.intellij.ui.GuiUtils;
+import com.intellij.ui.IdeBorderFactory;
+import com.intellij.ui.ToolbarDecorator;
+import com.intellij.ui.table.JBTable;
+import java.awt.BorderLayout;
+import java.awt.Dimension;
+import java.awt.event.ActionListener;
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+import java.util.stream.Collectors;
+import javax.annotation.Nullable;
+import javax.swing.DefaultCellEditor;
+import javax.swing.JComponent;
+import javax.swing.JPanel;
+import javax.swing.JTable;
+import javax.swing.table.TableColumn;
+import org.jdom.output.Format;
+import org.jdom.output.XMLOutputter;
+
+/** UI for exporting blaze run configurations. */
+public class ExportRunConfigurationDialog extends DialogWrapper {
+
+  private final ImmutableList<RunConfiguration> blazeConfigurations;
+  private final ExportRunConfigurationTableModel tableModel;
+  private final JBTable table;
+  private final FieldPanel outputDirectoryPanel;
+
+  ExportRunConfigurationDialog(Project project) {
+    super(project, true);
+    blazeConfigurations =
+        ImmutableList.copyOf(
+            RunManager.getInstance(project)
+                .getAllConfigurationsList()
+                .stream()
+                .filter((config) -> config instanceof BlazeRunConfiguration)
+                .collect(Collectors.toList()));
+    tableModel = new ExportRunConfigurationTableModel(blazeConfigurations);
+    table = new JBTable(tableModel);
+
+    TableColumn booleanColumn = table.getColumnModel().getColumn(0);
+    booleanColumn.setCellRenderer(new BooleanTableCellRenderer());
+    booleanColumn.setCellEditor(new BooleanTableCellEditor());
+    int width = table.getFontMetrics(table.getFont()).stringWidth(table.getColumnName(0)) + 10;
+    booleanColumn.setPreferredWidth(width);
+    booleanColumn.setMinWidth(width);
+    booleanColumn.setMaxWidth(width);
+
+    table
+        .getColumnModel()
+        .getColumn(2)
+        .setCellEditor(new DefaultCellEditor(GuiUtils.createUndoableTextField()));
+
+    TableColumn nameColumn = table.getColumnModel().getColumn(1);
+    nameColumn.setCellRenderer(
+        new ColoredTableCellRenderer() {
+          @Override
+          protected void customizeCellRenderer(
+              JTable table, Object value, boolean isSelected, boolean hasFocus, int row, int col) {
+            RunConfiguration config = blazeConfigurations.get(row);
+            setIcon(config.getType().getIcon());
+            append(config.getName());
+          }
+        });
+
+    table.setPreferredSize(new Dimension(700, 700));
+    table.setShowColumns(true);
+
+    final ActionListener browseAction = e -> chooseDirectory();
+    outputDirectoryPanel =
+        new FieldPanel("Export configurations to directory:", null, browseAction, null);
+    File defaultExportDirectory = defaultExportDirectory(project);
+    if (defaultExportDirectory != null) {
+      outputDirectoryPanel.setText(defaultExportDirectory.getPath());
+    }
+
+    String buildSystem = Blaze.buildSystemName(project);
+    setTitle(String.format("Export %s Run Configurations", buildSystem));
+    init();
+  }
+
+  /** Try to find a checked-in project view file. Otherwise, fall back to the workspace root. */
+  @Nullable
+  private static File defaultExportDirectory(Project project) {
+    WorkspaceRoot workspaceRoot = WorkspaceRoot.fromProjectSafe(project);
+    if (workspaceRoot == null) {
+      return null;
+    }
+    ProjectViewSet projectViewSet = ProjectViewManager.getInstance(project).getProjectViewSet();
+    if (projectViewSet != null) {
+      for (ProjectViewFile projectViewFile : projectViewSet.getProjectViewFiles()) {
+        File file = projectViewFile.projectViewFile;
+        if (file != null && FileUtil.isAncestor(workspaceRoot.directory(), file, false)) {
+          return file.getParentFile();
+        }
+      }
+    }
+    return workspaceRoot.directory();
+  }
+
+  private String getOutputDirectoryPath() {
+    return Strings.nullToEmpty(outputDirectoryPanel.getText()).trim();
+  }
+
+  private void chooseDirectory() {
+    FileChooserDescriptor descriptor =
+        FileChooserDescriptorFactory.createSingleFolderDescriptor()
+            .withTitle("Export Directory Location")
+            .withDescription("Choose directory to export run configurations to")
+            .withHideIgnored(false);
+    FileChooserDialog chooser =
+        FileChooserFactory.getInstance().createFileChooser(descriptor, null, null);
+
+    final VirtualFile[] files;
+    File existingLocation = new File(getOutputDirectoryPath());
+    if (existingLocation.exists()) {
+      VirtualFile toSelect =
+          LocalFileSystem.getInstance().refreshAndFindFileByPath(existingLocation.getPath());
+      files = chooser.choose(null, toSelect);
+    } else {
+      files = chooser.choose(null);
+    }
+    if (files.length == 0) {
+      return;
+    }
+    VirtualFile file = files[0];
+    outputDirectoryPanel.setText(file.getPath());
+  }
+
+  @Nullable
+  @Override
+  protected ValidationInfo doValidate() {
+    String outputDir = getOutputDirectoryPath();
+    if (outputDir.isEmpty()) {
+      return new ValidationInfo("Choose an output directory");
+    }
+    if (!FileAttributeProvider.getInstance().exists(new File(outputDir))) {
+      return new ValidationInfo("Invalid output directory");
+    }
+    Set<String> names = new HashSet<>();
+    for (int i = 0; i < blazeConfigurations.size(); i++) {
+      if (!tableModel.enabled[i]) {
+        continue;
+      }
+      if (!names.add(tableModel.paths[i])) {
+        return new ValidationInfo("Duplicate output file name '" + tableModel.paths[i] + "'");
+      }
+    }
+    return null;
+  }
+
+  @Override
+  protected void doOKAction() {
+    File outputDir = new File(getOutputDirectoryPath());
+    List<File> outputFiles = new ArrayList<>();
+    for (int i = 0; i < blazeConfigurations.size(); i++) {
+      if (!tableModel.enabled[i]) {
+        continue;
+      }
+      File outputFile = new File(outputDir, tableModel.paths[i]);
+      writeConfiguration(blazeConfigurations.get(i), outputFile);
+      outputFiles.add(outputFile);
+    }
+    LocalFileSystem.getInstance().refreshIoFiles(outputFiles);
+    super.doOKAction();
+  }
+
+  private static void writeConfiguration(RunConfiguration configuration, File outputFile) {
+    try (FileOutputStream writer = new FileOutputStream(outputFile, false)) {
+      XMLOutputter xmlOutputter = new XMLOutputter(Format.getCompactFormat());
+      xmlOutputter.output(RunConfigurationSerializer.writeToXml(configuration), writer);
+    } catch (IOException e) {
+      throw new RuntimeException("Error exporting run configuration to file: " + outputFile);
+    }
+  }
+
+  @Override
+  protected JComponent createNorthPanel() {
+    return outputDirectoryPanel;
+  }
+
+  @Override
+  protected JComponent createCenterPanel() {
+    JPanel panel = new JPanel(new BorderLayout());
+    panel.setBorder(IdeBorderFactory.createTitledBorder("Run Configurations", false));
+    panel.add(
+        ToolbarDecorator.createDecorator(table).addExtraAction(new SelectAllButton()).createPanel(),
+        BorderLayout.CENTER);
+    return panel;
+  }
+
+  private class SelectAllButton extends AnActionButton {
+
+    boolean allSelected = false;
+
+    private SelectAllButton() {
+      super("Select All", AllIcons.Actions.Selectall);
+    }
+
+    @Override
+    public synchronized void actionPerformed(AnActionEvent anActionEvent) {
+      boolean newState = !allSelected;
+      for (int i = 0; i < tableModel.enabled.length; i++) {
+        table.setValueAt(newState, i, 0);
+      }
+      allSelected = newState;
+      Presentation presentation = anActionEvent.getPresentation();
+      if (allSelected) {
+        presentation.setText("Deselect All");
+        presentation.setIcon(AllIcons.Actions.Unselectall);
+      } else {
+        presentation.setText("Select All");
+        presentation.setIcon(AllIcons.Actions.Selectall);
+      }
+      tableModel.fireTableDataChanged();
+      table.revalidate();
+      table.repaint();
+    }
+  }
+}
diff --git a/base/src/com/google/idea/blaze/base/run/exporter/ExportRunConfigurationTableModel.java b/base/src/com/google/idea/blaze/base/run/exporter/ExportRunConfigurationTableModel.java
new file mode 100644
index 0000000..cc00d52
--- /dev/null
+++ b/base/src/com/google/idea/blaze/base/run/exporter/ExportRunConfigurationTableModel.java
@@ -0,0 +1,107 @@
+/*
+ * 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.run.exporter;
+
+import com.google.common.collect.ImmutableList;
+import com.intellij.execution.configurations.RunConfiguration;
+import com.intellij.openapi.util.io.FileUtil;
+import com.intellij.util.text.UniqueNameGenerator;
+import java.util.List;
+import javax.swing.table.AbstractTableModel;
+
+/** Table model used by the 'export run configurations' UI. */
+class ExportRunConfigurationTableModel extends AbstractTableModel {
+
+  private static final ImmutableList<String> COLUMN_NAMES =
+      ImmutableList.of("Export", "Name", "Output filename");
+  private static final ImmutableList<Class<?>> COLUMN_CLASSES =
+      ImmutableList.of(Boolean.class, String.class, String.class);
+
+  final Boolean[] enabled;
+  final String[] names;
+  final String[] paths;
+
+  ExportRunConfigurationTableModel(List<RunConfiguration> configurations) {
+    enabled = new Boolean[configurations.size()];
+    names = new String[configurations.size()];
+    paths = new String[configurations.size()];
+
+    UniqueNameGenerator nameGenerator = new UniqueNameGenerator();
+    for (int i = 0; i < configurations.size(); i++) {
+      RunConfiguration config = configurations.get(i);
+      enabled[i] = false;
+      names[i] = config.getName();
+      paths[i] =
+          nameGenerator.generateUniqueName(FileUtil.sanitizeFileName(config.getName()), "", ".xml");
+    }
+  }
+
+  @Override
+  public int getColumnCount() {
+    return 3;
+  }
+
+  @Override
+  public Class<?> getColumnClass(int columnIndex) {
+    return COLUMN_CLASSES.get(columnIndex);
+  }
+
+  @Override
+  public String getColumnName(int column) {
+    return COLUMN_NAMES.get(column);
+  }
+
+  @Override
+  public int getRowCount() {
+    return enabled.length;
+  }
+
+  @Override
+  public boolean isCellEditable(int rowIndex, int columnIndex) {
+    return columnIndex != 1;
+  }
+
+  @Override
+  public Object getValueAt(int rowIndex, int columnIndex) {
+    switch (columnIndex) {
+      case 0:
+        return enabled[rowIndex];
+      case 1:
+        return names[rowIndex];
+      case 2:
+        return paths[rowIndex];
+      default:
+        throw new RuntimeException("Invalid column index: " + columnIndex);
+    }
+  }
+
+  @Override
+  public void setValueAt(Object aValue, int rowIndex, int columnIndex) {
+    switch (columnIndex) {
+      case 0:
+        enabled[rowIndex] = (Boolean) aValue;
+        return;
+      case 1:
+        names[rowIndex] = (String) aValue;
+        return;
+      case 2:
+        paths[rowIndex] = (String) aValue;
+        return;
+      default:
+        throw new RuntimeException("Invalid column index: " + columnIndex);
+    }
+  }
+}
diff --git a/base/src/com/google/idea/blaze/base/run/exporter/RunConfigurationSerializer.java b/base/src/com/google/idea/blaze/base/run/exporter/RunConfigurationSerializer.java
new file mode 100644
index 0000000..aed9db4
--- /dev/null
+++ b/base/src/com/google/idea/blaze/base/run/exporter/RunConfigurationSerializer.java
@@ -0,0 +1,125 @@
+/*
+ * 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.run.exporter;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.idea.blaze.base.run.BlazeRunConfiguration;
+import com.intellij.execution.RunnerAndConfigurationSettings;
+import com.intellij.execution.configurations.RunConfiguration;
+import com.intellij.execution.impl.RunManagerImpl;
+import com.intellij.execution.impl.RunnerAndConfigurationSettingsImpl;
+import com.intellij.openapi.diagnostic.Logger;
+import com.intellij.openapi.project.Project;
+import com.intellij.openapi.util.InvalidDataException;
+import com.intellij.openapi.util.JDOMUtil;
+import com.intellij.openapi.util.WriteExternalException;
+import java.io.File;
+import java.io.IOException;
+import javax.annotation.Nullable;
+import org.jdom.Element;
+import org.jdom.JDOMException;
+
+/** Utility methods for converting run configuration to/from XML. */
+public class RunConfigurationSerializer {
+
+  private static final Logger logger = Logger.getInstance(RunConfigurationSerializer.class);
+
+  public static Element writeToXml(RunConfiguration configuration) {
+    RunnerAndConfigurationSettings settings =
+        RunManagerImpl.getInstanceImpl(configuration.getProject()).getSettings(configuration);
+    Element element = new Element("configuration");
+    try {
+      ((RunnerAndConfigurationSettingsImpl) settings).writeExternal(element);
+    } catch (WriteExternalException e) {
+      logger.warn("Error serializing run configuration to XML", e);
+    }
+    return element;
+  }
+
+  /**
+   * Parses a RunConfiguration from the given XML file, and adds it to the project, if there's not
+   * already a run configuration with the same name and type,
+   */
+  public static void loadFromXmlIgnoreExisting(Project project, File xmlFile) {
+    try {
+      loadFromXmlElementIgnoreExisting(project, JDOMUtil.load(xmlFile));
+    } catch (InvalidDataException | JDOMException | IOException e) {
+      logger.warn("Error parsing run configuration from XML", e);
+    }
+  }
+
+  /**
+   * Parses a RunConfiguration from the given XML element, and adds it to the project, if there's
+   * not already a run configuration with the same name and type,
+   */
+  @VisibleForTesting
+  static void loadFromXmlElementIgnoreExisting(Project project, Element element)
+      throws InvalidDataException {
+    if (!shouldLoadConfiguration(project, element)) {
+      return;
+    }
+    RunnerAndConfigurationSettings settings =
+        RunManagerImpl.getInstanceImpl(project).loadConfiguration(element, false);
+    RunConfiguration config = settings != null ? settings.getConfiguration() : null;
+    if (config instanceof BlazeRunConfiguration) {
+      ((BlazeRunConfiguration) config).setKeepInSync(true);
+    }
+  }
+
+  /**
+   * Deserializes the configuration represented by the given XML element, then searches for an
+   * existing run configuration in the project with the same name and type.
+   */
+  @Nullable
+  @VisibleForTesting
+  static RunnerAndConfigurationSettings findExisting(Project project, Element element)
+      throws InvalidDataException {
+    RunManagerImpl manager = RunManagerImpl.getInstanceImpl(project);
+    RunnerAndConfigurationSettingsImpl settings = new RunnerAndConfigurationSettingsImpl(manager);
+    settings.readExternal(element);
+    RunConfiguration config = settings.getConfiguration();
+    if (config == null) {
+      return null;
+    }
+    return manager.findConfigurationByTypeAndName(config.getType().getId(), config.getName());
+  }
+
+  /**
+   * Returns true if there's either no matching configuration altready in the project, or the
+   * matching configuration is marked as 'keep in sync'.
+   */
+  @VisibleForTesting
+  static boolean shouldLoadConfiguration(Project project, Element element)
+      throws InvalidDataException {
+    RunnerAndConfigurationSettings existing = findExisting(project, element);
+    if (existing == null) {
+      return true;
+    }
+    RunConfiguration config = existing.getConfiguration();
+    if (!(config instanceof BlazeRunConfiguration)) {
+      return false;
+    }
+    BlazeRunConfiguration blazeConfig = (BlazeRunConfiguration) config;
+    Boolean keepInSync = blazeConfig.getKeepInSync();
+    if (keepInSync == null) {
+      // if the matching configuration was never previously imported, don't overwrite, but activate
+      // the UI option to keep it in sync.
+      blazeConfig.setKeepInSync(false);
+      return false;
+    }
+    return keepInSync;
+  }
+}
diff --git a/base/src/com/google/idea/blaze/base/run/smrunner/BlazeRerunFailedTestsAction.java b/base/src/com/google/idea/blaze/base/run/smrunner/BlazeRerunFailedTestsAction.java
new file mode 100644
index 0000000..bc90989
--- /dev/null
+++ b/base/src/com/google/idea/blaze/base/run/smrunner/BlazeRerunFailedTestsAction.java
@@ -0,0 +1,100 @@
+/*
+ * 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.run.smrunner;
+
+import com.google.idea.blaze.base.command.BlazeCommandName;
+import com.google.idea.blaze.base.command.BlazeFlags;
+import com.google.idea.blaze.base.run.BlazeCommandRunConfiguration;
+import com.google.idea.blaze.base.run.state.BlazeCommandRunConfigurationCommonState;
+import com.intellij.execution.ExecutionException;
+import com.intellij.execution.Executor;
+import com.intellij.execution.configurations.RunProfileState;
+import com.intellij.execution.runners.ExecutionEnvironment;
+import com.intellij.execution.testframework.TestFrameworkRunningModel;
+import com.intellij.execution.testframework.actions.AbstractRerunFailedTestsAction;
+import com.intellij.openapi.module.Module;
+import com.intellij.openapi.ui.ComponentContainer;
+import java.util.ArrayList;
+import java.util.List;
+import javax.annotation.Nullable;
+
+/** Re-run failed tests. */
+public class BlazeRerunFailedTestsAction extends AbstractRerunFailedTestsAction {
+
+  private final BlazeTestEventsHandler eventsHandler;
+
+  public BlazeRerunFailedTestsAction(
+      BlazeTestEventsHandler eventsHandler, ComponentContainer componentContainer) {
+    super(componentContainer);
+    this.eventsHandler = eventsHandler;
+  }
+
+  @Override
+  @Nullable
+  protected MyRunProfile getRunProfile(ExecutionEnvironment environment) {
+    final TestFrameworkRunningModel model = getModel();
+    if (model == null) {
+      return null;
+    }
+    BlazeCommandRunConfiguration config =
+        (BlazeCommandRunConfiguration) model.getProperties().getConfiguration();
+    return new BlazeRerunTestRunProfile(config.clone());
+  }
+
+  class BlazeRerunTestRunProfile extends MyRunProfile {
+
+    private final BlazeCommandRunConfiguration configuration;
+
+    BlazeRerunTestRunProfile(BlazeCommandRunConfiguration configuration) {
+      super(configuration);
+      this.configuration = configuration;
+    }
+
+    @Override
+    public Module[] getModules() {
+      return Module.EMPTY_ARRAY;
+    }
+
+    @Nullable
+    @Override
+    public RunProfileState getState(Executor executor, ExecutionEnvironment environment)
+        throws ExecutionException {
+      BlazeCommandRunConfigurationCommonState handlerState =
+          configuration.getHandlerStateIfType(BlazeCommandRunConfigurationCommonState.class);
+      if (handlerState == null || !BlazeCommandName.TEST.equals(handlerState.getCommand())) {
+        return null;
+      }
+      String testFilter = eventsHandler.getTestFilter(getProject(), getFailedTests(getProject()));
+      List<String> blazeFlags = setTestFilter(handlerState.getBlazeFlags(), testFilter);
+      handlerState.setBlazeFlags(blazeFlags);
+      return configuration.getState(executor, environment);
+    }
+
+    /** Replaces existing test_filter flag, or appends if none exists. */
+    private List<String> setTestFilter(List<String> flags, String testFilter) {
+      List<String> copy = new ArrayList<>(flags);
+      for (int i = 0; i < copy.size(); i++) {
+        String flag = copy.get(i);
+        if (flag.startsWith(BlazeFlags.TEST_FILTER)) {
+          copy.set(i, testFilter);
+          return copy;
+        }
+      }
+      copy.add(testFilter);
+      return copy;
+    }
+  }
+}
diff --git a/base/src/com/google/idea/blaze/base/run/smrunner/BlazeTestConsoleProperties.java b/base/src/com/google/idea/blaze/base/run/smrunner/BlazeTestConsoleProperties.java
new file mode 100644
index 0000000..6582c23
--- /dev/null
+++ b/base/src/com/google/idea/blaze/base/run/smrunner/BlazeTestConsoleProperties.java
@@ -0,0 +1,57 @@
+/*
+ * 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.run.smrunner;
+
+import com.intellij.execution.Executor;
+import com.intellij.execution.configurations.RunConfiguration;
+import com.intellij.execution.testframework.TestConsoleProperties;
+import com.intellij.execution.testframework.actions.AbstractRerunFailedTestsAction;
+import com.intellij.execution.testframework.sm.SMCustomMessagesParsing;
+import com.intellij.execution.testframework.sm.runner.OutputToGeneralTestEventsConverter;
+import com.intellij.execution.testframework.sm.runner.SMTRunnerConsoleProperties;
+import com.intellij.execution.testframework.sm.runner.SMTestLocator;
+import com.intellij.execution.ui.ConsoleView;
+import javax.annotation.Nullable;
+
+/** Integrates blaze test results with the SM-runner test UI. */
+public class BlazeTestConsoleProperties extends SMTRunnerConsoleProperties
+    implements SMCustomMessagesParsing {
+
+  private final BlazeTestEventsHandler eventsHandler;
+
+  public BlazeTestConsoleProperties(
+      RunConfiguration runConfiguration, Executor executor, BlazeTestEventsHandler eventsHandler) {
+    super(runConfiguration, eventsHandler.frameworkName, executor);
+    this.eventsHandler = eventsHandler;
+  }
+
+  @Override
+  public OutputToGeneralTestEventsConverter createTestEventsConverter(
+      String framework, TestConsoleProperties consoleProperties) {
+    return new BlazeXmlToTestEventsConverter(framework, consoleProperties, eventsHandler);
+  }
+
+  @Override
+  public SMTestLocator getTestLocator() {
+    return eventsHandler.getTestLocator();
+  }
+
+  @Nullable
+  @Override
+  public AbstractRerunFailedTestsAction createRerunFailedTestsAction(ConsoleView consoleView) {
+    return eventsHandler.createRerunFailedTestsAction(consoleView);
+  }
+}
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
new file mode 100644
index 0000000..87e1b7f
--- /dev/null
+++ b/base/src/com/google/idea/blaze/base/run/smrunner/BlazeTestEventsHandler.java
@@ -0,0 +1,103 @@
+/*
+ * 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.run.smrunner;
+
+import com.google.common.base.Strings;
+import com.google.common.collect.ImmutableList;
+import com.intellij.execution.testframework.AbstractTestProxy;
+import com.intellij.execution.testframework.actions.AbstractRerunFailedTestsAction;
+import com.intellij.execution.testframework.sm.runner.SMTestLocator;
+import com.intellij.execution.ui.ConsoleView;
+import com.intellij.openapi.project.Project;
+import com.intellij.util.io.URLUtil;
+import java.io.File;
+import java.io.IOException;
+import java.nio.file.Files;
+import java.util.List;
+import javax.annotation.Nullable;
+
+/** Language-specific handling of SM runner test protocol */
+public abstract class BlazeTestEventsHandler {
+
+  public final String frameworkName;
+  public final File testOutputXml;
+
+  protected BlazeTestEventsHandler(String frameworkName) {
+    this.frameworkName = frameworkName;
+    this.testOutputXml = generateTempTestXmlFile();
+  }
+
+  /**
+   * Blaze/Bazel flags required for test UI.<br>
+   * Forces local test execution, without sharding, and sets the output test xml path.
+   */
+  public ImmutableList<String> getBlazeFlags() {
+    return ImmutableList.of(
+        "--test_env=XML_OUTPUT_FILE=" + testOutputXml,
+        "--test_sharding_strategy=disabled",
+        "--runs_per_test=1",
+        "--flaky_test_attempts=1",
+        "--test_strategy=local");
+  }
+
+  public abstract SMTestLocator getTestLocator();
+
+  /**
+   * The --test_filter flag passed to blaze to rerun the given tests.
+   *
+   * @return null if no filter can be constructed for these tests.
+   */
+  @Nullable
+  public abstract String getTestFilter(Project project, List<AbstractTestProxy> failedTests);
+
+  @Nullable
+  public AbstractRerunFailedTestsAction createRerunFailedTestsAction(ConsoleView consoleView) {
+    return new BlazeRerunFailedTestsAction(this, consoleView);
+  }
+
+  /** Converts the testsuite name in the blaze test XML to a user-friendly format */
+  public String suiteDisplayName(String rawName) {
+    return rawName;
+  }
+
+  /** Converts the testcase name in the blaze test XML to a user-friendly format */
+  public String testDisplayName(String rawName) {
+    return rawName;
+  }
+
+  public String suiteLocationUrl(String name) {
+    return SmRunnerUtils.GENERIC_SUITE_PROTOCOL + URLUtil.SCHEME_SEPARATOR + name;
+  }
+
+  public String testLocationUrl(String name, @Nullable String className) {
+    String base = SmRunnerUtils.GENERIC_TEST_PROTOCOL + URLUtil.SCHEME_SEPARATOR + name;
+    if (Strings.isNullOrEmpty(className)) {
+      return base;
+    }
+    return base + SmRunnerUtils.TEST_NAME_PARTS_SPLITTER + className;
+  }
+
+  private static File generateTempTestXmlFile() {
+    try {
+      File file = Files.createTempFile("blazeTest", ".xml").toFile();
+      file.deleteOnExit();
+      return file;
+
+    } catch (IOException e) {
+      throw new RuntimeException(e);
+    }
+  }
+}
diff --git a/aswb/src/com/google/idea/blaze/android/run/binary/instantrun/InstantRunExperiment.java b/base/src/com/google/idea/blaze/base/run/smrunner/BlazeTestEventsHandlerProvider.java
similarity index 61%
copy from aswb/src/com/google/idea/blaze/android/run/binary/instantrun/InstantRunExperiment.java
copy to base/src/com/google/idea/blaze/base/run/smrunner/BlazeTestEventsHandlerProvider.java
index 148b1d9..c02a0cb 100644
--- a/aswb/src/com/google/idea/blaze/android/run/binary/instantrun/InstantRunExperiment.java
+++ b/base/src/com/google/idea/blaze/base/run/smrunner/BlazeTestEventsHandlerProvider.java
@@ -5,7 +5,7 @@
  * 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
+ *    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,
@@ -13,12 +13,10 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package com.google.idea.blaze.android.run.binary.instantrun;
+package com.google.idea.blaze.base.run.smrunner;
 
-import com.google.idea.common.experiments.BoolExperiment;
+/** Provides a {@link BlazeTestEventsHandler}. */
+public interface BlazeTestEventsHandlerProvider {
 
-/** Holds the instant run experiment */
-public class InstantRunExperiment {
-  public static final BoolExperiment INSTANT_RUN_ENABLED =
-      new BoolExperiment("instant.run.enabled", false);
+  BlazeTestEventsHandler getHandler();
 }
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
new file mode 100644
index 0000000..8f6d1f0
--- /dev/null
+++ b/base/src/com/google/idea/blaze/base/run/smrunner/BlazeXmlSchema.java
@@ -0,0 +1,110 @@
+/*
+ * 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.run.smrunner;
+
+import com.google.common.collect.Lists;
+import java.io.InputStream;
+import java.util.List;
+import javax.xml.bind.JAXBContext;
+import javax.xml.bind.JAXBException;
+import javax.xml.bind.annotation.XmlAttribute;
+import javax.xml.bind.annotation.XmlElement;
+import javax.xml.bind.annotation.XmlRootElement;
+import javax.xml.bind.annotation.XmlValue;
+
+/** Used to parse the test.xml generated by the blaze/bazel testing framework. */
+public class BlazeXmlSchema {
+
+  private static final JAXBContext CONTEXT;
+
+  static {
+    try {
+      CONTEXT = JAXBContext.newInstance(TestSuite.class);
+    } catch (JAXBException e) {
+      throw new RuntimeException(e);
+    }
+  }
+
+  static TestSuite parse(InputStream input) {
+    try {
+      return (TestSuite) CONTEXT.createUnmarshaller().unmarshal(input);
+    } catch (JAXBException e) {
+      throw new RuntimeException("Failed to parse test XML", e);
+    }
+  }
+
+  @XmlRootElement(name = "testsuites")
+  static class TestSuite {
+    @XmlAttribute String name;
+    @XmlAttribute String classname;
+    @XmlAttribute int tests;
+    @XmlAttribute int failures;
+    @XmlAttribute int errors;
+    @XmlAttribute int skipped;
+    @XmlAttribute int disabled;
+    @XmlAttribute double time;
+
+    @XmlAttribute(name = "system-out")
+    String sysOut;
+
+    @XmlAttribute(name = "system-err")
+    String sysErr;
+
+    @XmlElement(name = "error", type = ErrorOrFailureOrSkipped.class)
+    ErrorOrFailureOrSkipped error;
+
+    @XmlElement(name = "failure", type = ErrorOrFailureOrSkipped.class)
+    ErrorOrFailureOrSkipped failure;
+
+    @XmlElement(name = "testsuite")
+    List<TestSuite> testSuites = Lists.newArrayList();
+
+    @XmlElement(name = "testdecorator")
+    List<TestSuite> testDecorators = Lists.newArrayList();
+
+    @XmlElement(name = "testcase")
+    List<TestCase> testCases = Lists.newArrayList();
+  }
+
+  static class TestCase {
+    @XmlAttribute String name;
+    @XmlAttribute String classname;
+    @XmlAttribute String status;
+    @XmlAttribute String result;
+    @XmlAttribute String time;
+
+    @XmlAttribute(name = "system-out")
+    String sysOut;
+
+    @XmlAttribute(name = "system-err")
+    String sysErr;
+
+    @XmlElement(name = "error", type = ErrorOrFailureOrSkipped.class)
+    ErrorOrFailureOrSkipped error;
+
+    @XmlElement(name = "failure", type = ErrorOrFailureOrSkipped.class)
+    ErrorOrFailureOrSkipped failure;
+
+    @XmlElement(name = "skipped", type = ErrorOrFailureOrSkipped.class)
+    ErrorOrFailureOrSkipped skipped;
+  }
+
+  static class ErrorOrFailureOrSkipped {
+    @XmlValue String content;
+    @XmlAttribute String message;
+    @XmlAttribute String type;
+  }
+}
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
new file mode 100644
index 0000000..82dcae8
--- /dev/null
+++ b/base/src/com/google/idea/blaze/base/run/smrunner/BlazeXmlToTestEventsConverter.java
@@ -0,0 +1,207 @@
+/*
+ * 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.run.smrunner;
+
+import com.google.idea.blaze.base.run.smrunner.BlazeXmlSchema.ErrorOrFailureOrSkipped;
+import com.google.idea.blaze.base.run.smrunner.BlazeXmlSchema.TestCase;
+import com.google.idea.blaze.base.run.smrunner.BlazeXmlSchema.TestSuite;
+import com.google.idea.sdkcompat.smrunner.SmRunnerCompatUtils;
+import com.intellij.execution.process.ProcessOutputTypes;
+import com.intellij.execution.testframework.TestConsoleProperties;
+import com.intellij.execution.testframework.sm.runner.GeneralTestEventsProcessor;
+import com.intellij.execution.testframework.sm.runner.OutputToGeneralTestEventsConverter;
+import com.intellij.execution.testframework.sm.runner.events.TestFinishedEvent;
+import com.intellij.execution.testframework.sm.runner.events.TestIgnoredEvent;
+import com.intellij.execution.testframework.sm.runner.events.TestOutputEvent;
+import com.intellij.execution.testframework.sm.runner.events.TestStartedEvent;
+import com.intellij.execution.testframework.sm.runner.events.TestSuiteFinishedEvent;
+import com.intellij.execution.testframework.sm.runner.events.TestSuiteStartedEvent;
+import com.intellij.openapi.util.Key;
+import java.io.FileInputStream;
+import java.io.InputStream;
+import java.text.ParseException;
+import javax.annotation.Nullable;
+import jetbrains.buildServer.messages.serviceMessages.ServiceMessageVisitor;
+import jetbrains.buildServer.messages.serviceMessages.TestSuiteStarted;
+
+/** Converts blaze test runner xml logs to smRunner events. */
+public class BlazeXmlToTestEventsConverter extends OutputToGeneralTestEventsConverter {
+
+  private static final ErrorOrFailureOrSkipped NO_ERROR = new ErrorOrFailureOrSkipped();
+
+  private final BlazeTestEventsHandler eventsHandler;
+
+  public BlazeXmlToTestEventsConverter(
+      String testFrameworkName,
+      TestConsoleProperties testConsoleProperties,
+      BlazeTestEventsHandler eventsHandler) {
+    super(testFrameworkName, testConsoleProperties);
+    this.eventsHandler = eventsHandler;
+  }
+
+  @Override
+  protected boolean processServiceMessages(
+      String s, Key key, ServiceMessageVisitor serviceMessageVisitor) throws ParseException {
+    return super.processServiceMessages(s, key, serviceMessageVisitor);
+  }
+
+  @Override
+  public void process(String text, Key outputType) {
+    super.process(text, outputType);
+  }
+
+  @Override
+  public void dispose() {
+    super.dispose();
+  }
+
+  @Override
+  public void flushBufferBeforeTerminating() {
+    super.flushBufferBeforeTerminating();
+    onStartTesting();
+    try (InputStream input = new FileInputStream(eventsHandler.testOutputXml)) {
+      parseXmlInput(getProcessor(), input);
+    } catch (Exception e) {
+      // ignore parsing errors -- most common cause is user cancellation, which we can't easily
+      // recognize.
+    }
+  }
+
+  private void parseXmlInput(GeneralTestEventsProcessor processor, InputStream input) {
+    TestSuite testResult = BlazeXmlSchema.parse(input);
+    processor.onTestsReporterAttached();
+    processTestSuite(processor, testResult);
+  }
+
+  private void processTestSuite(GeneralTestEventsProcessor processor, TestSuite suite) {
+    if (!hasRunChild(suite)) {
+      return;
+    }
+    // don't include the outermost 'testsuites' element.
+    boolean logSuite = suite.testSuites.isEmpty();
+    if (suite.name != null && logSuite) {
+      TestSuiteStarted suiteStarted =
+          new TestSuiteStarted(eventsHandler.suiteDisplayName(suite.name));
+      String locationUrl = eventsHandler.suiteLocationUrl(suite.name);
+      processor.onSuiteStarted(new TestSuiteStartedEvent(suiteStarted, locationUrl));
+    }
+
+    for (TestSuite child : suite.testSuites) {
+      processTestSuite(processor, child);
+    }
+    for (TestSuite decorator : suite.testDecorators) {
+      processTestSuite(processor, decorator);
+    }
+    for (TestCase test : suite.testCases) {
+      processTestCase(processor, test);
+    }
+
+    if (suite.sysOut != null) {
+      processor.onUncapturedOutput(suite.sysOut, ProcessOutputTypes.STDOUT);
+    }
+    if (suite.sysErr != null) {
+      processor.onUncapturedOutput(suite.sysErr, ProcessOutputTypes.STDERR);
+    }
+
+    if (suite.name != null && logSuite) {
+      processor.onSuiteFinished(
+          new TestSuiteFinishedEvent(eventsHandler.suiteDisplayName(suite.name)));
+    }
+  }
+
+  /**
+   * Does the test suite have at least one child which wasn't skipped? <br>
+   * This prevents spurious warnings from entirely filtered test classes.
+   */
+  private static boolean hasRunChild(TestSuite suite) {
+    for (TestSuite child : suite.testSuites) {
+      if (hasRunChild(child)) {
+        return true;
+      }
+    }
+    for (TestSuite child : suite.testDecorators) {
+      if (hasRunChild(child)) {
+        return true;
+      }
+    }
+    for (TestCase test : suite.testCases) {
+      if ("run".equals(test.status)) {
+        return true;
+      }
+    }
+    return false;
+  }
+
+  private static boolean isCancelled(TestCase test) {
+    return "interrupted".equalsIgnoreCase(test.result) || "cancelled".equalsIgnoreCase(test.result);
+  }
+
+  private static boolean isIgnored(TestCase test) {
+    if (test.skipped != null) {
+      return true;
+    }
+    return "suppressed".equalsIgnoreCase(test.result)
+        || "skipped".equalsIgnoreCase(test.result)
+        || "filtered".equalsIgnoreCase(test.result);
+  }
+
+  private static boolean isFailed(TestCase test) {
+    return test.failure != null || test.error != null;
+  }
+
+  private void processTestCase(GeneralTestEventsProcessor processor, TestCase test) {
+    if (test.name == null || "notrun".equals(test.status) || isCancelled(test)) {
+      return;
+    }
+    String displayName = eventsHandler.testDisplayName(test.name);
+    String locationUrl = eventsHandler.testLocationUrl(test.name, test.classname);
+    processor.onTestStarted(new TestStartedEvent(displayName, locationUrl));
+
+    if (test.sysOut != null) {
+      processor.onTestOutput(new TestOutputEvent(displayName, test.sysOut, true));
+    }
+    if (test.sysErr != null) {
+      processor.onTestOutput(new TestOutputEvent(displayName, test.sysErr, true));
+    }
+
+    if (isIgnored(test)) {
+      ErrorOrFailureOrSkipped err = test.skipped != null ? test.skipped : NO_ERROR;
+      processor.onTestIgnored(new TestIgnoredEvent(displayName, err.message, err.content));
+    } else if (isFailed(test)) {
+      ErrorOrFailureOrSkipped err =
+          test.failure != null ? test.failure : test.error != null ? test.error : NO_ERROR;
+      processor.onTestFailure(
+          SmRunnerCompatUtils.getTestFailedEvent(
+              displayName, err.message, err.content, parseTimeMillis(test.time)));
+    }
+    processor.onTestFinished(new TestFinishedEvent(displayName, parseTimeMillis(test.time)));
+  }
+
+  private static long parseTimeMillis(@Nullable String time) {
+    if (time == null) {
+      return -1;
+    }
+    // if the number contains a decimal point, it's a value in seconds. Otherwise in milliseconds.
+    try {
+      if (time.contains(".")) {
+        return Math.round(Float.parseFloat(time) * 1000);
+      }
+      return Long.parseLong(time);
+    } catch (NumberFormatException e) {
+      return -1;
+    }
+  }
+}
diff --git a/base/src/com/google/idea/blaze/base/run/smrunner/SmRunnerUtils.java b/base/src/com/google/idea/blaze/base/run/smrunner/SmRunnerUtils.java
new file mode 100644
index 0000000..04715a7
--- /dev/null
+++ b/base/src/com/google/idea/blaze/base/run/smrunner/SmRunnerUtils.java
@@ -0,0 +1,76 @@
+/*
+ * 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.run.smrunner;
+
+import com.intellij.execution.DefaultExecutionResult;
+import com.intellij.execution.Executor;
+import com.intellij.execution.configurations.RunConfiguration;
+import com.intellij.execution.testframework.TestConsoleProperties;
+import com.intellij.execution.testframework.actions.AbstractRerunFailedTestsAction;
+import com.intellij.execution.testframework.sm.SMTestRunnerConnectionUtil;
+import com.intellij.execution.testframework.sm.runner.SMTRunnerConsoleProperties;
+import com.intellij.execution.testframework.sm.runner.ui.SMTRunnerConsoleView;
+import com.intellij.execution.ui.ExecutionConsole;
+import com.intellij.openapi.project.Project;
+import com.intellij.openapi.util.Disposer;
+import javax.swing.tree.TreeSelectionModel;
+
+/** Utility methods for setting up the SM runner test UI. */
+public class SmRunnerUtils {
+
+  public static final String GENERIC_SUITE_PROTOCOL = "blaze:suite";
+  public static final String GENERIC_TEST_PROTOCOL = "blaze:test";
+  public static final String TEST_NAME_PARTS_SPLITTER = "::";
+
+  public static SMTRunnerConsoleView getConsoleView(
+      Project project,
+      RunConfiguration configuration,
+      Executor executor,
+      BlazeTestEventsHandler eventsHandler) {
+    SMTRunnerConsoleProperties properties =
+        new BlazeTestConsoleProperties(configuration, executor, eventsHandler);
+    SMTRunnerConsoleView console =
+        (SMTRunnerConsoleView)
+            SMTestRunnerConnectionUtil.createConsole(eventsHandler.frameworkName, properties);
+    Disposer.register(project, console);
+    console
+        .getResultsViewer()
+        .getTreeView()
+        .getSelectionModel()
+        .setSelectionMode(TreeSelectionModel.DISCONTIGUOUS_TREE_SELECTION);
+    return console;
+  }
+
+  public static DefaultExecutionResult attachRerunFailedTestsAction(DefaultExecutionResult result) {
+    ExecutionConsole console = result.getExecutionConsole();
+    if (!(console instanceof SMTRunnerConsoleView)) {
+      return result;
+    }
+    SMTRunnerConsoleView smConsole = (SMTRunnerConsoleView) console;
+    TestConsoleProperties consoleProperties = smConsole.getProperties();
+    if (!(consoleProperties instanceof BlazeTestConsoleProperties)) {
+      return result;
+    }
+    BlazeTestConsoleProperties properties = (BlazeTestConsoleProperties) consoleProperties;
+    AbstractRerunFailedTestsAction action = properties.createRerunFailedTestsAction(smConsole);
+    if (action != null) {
+      action.init(properties);
+      action.setModelProvider(smConsole::getResultsViewer);
+      result.setRestartActions(action);
+    }
+    return result;
+  }
+}
diff --git a/base/src/com/google/idea/blaze/base/run/state/BlazeBinaryState.java b/base/src/com/google/idea/blaze/base/run/state/BlazeBinaryState.java
index 2ddeffc..da96aad 100644
--- a/base/src/com/google/idea/blaze/base/run/state/BlazeBinaryState.java
+++ b/base/src/com/google/idea/blaze/base/run/state/BlazeBinaryState.java
@@ -72,6 +72,11 @@
     }
 
     @Override
+    public void setComponentEnabled(boolean enabled) {
+      blazeBinaryField.setEnabled(enabled);
+    }
+
+    @Override
     public void resetEditorFrom(RunConfigurationState genericState) {
       BlazeBinaryState state = (BlazeBinaryState) genericState;
       blazeBinaryField.setText(Strings.nullToEmpty(state.getBlazeBinary()));
diff --git a/base/src/com/google/idea/blaze/base/run/state/BlazeCommandRunConfigurationCommonState.java b/base/src/com/google/idea/blaze/base/run/state/BlazeCommandRunConfigurationCommonState.java
index ab44e47..1991ae6 100644
--- a/base/src/com/google/idea/blaze/base/run/state/BlazeCommandRunConfigurationCommonState.java
+++ b/base/src/com/google/idea/blaze/base/run/state/BlazeCommandRunConfigurationCommonState.java
@@ -17,8 +17,11 @@
 
 import com.google.idea.blaze.base.command.BlazeCommandName;
 import com.google.idea.blaze.base.command.BlazeFlags;
+import com.google.idea.blaze.base.run.state.BlazeRunOnDistributedExecutorState.RunOnExecutorStateEditor;
+import com.google.idea.blaze.base.settings.Blaze.BuildSystem;
 import com.intellij.execution.configurations.RuntimeConfigurationError;
 import com.intellij.execution.configurations.RuntimeConfigurationException;
+import com.intellij.openapi.project.Project;
 import java.io.File;
 import java.util.List;
 import javax.annotation.Nullable;
@@ -35,13 +38,15 @@
   private final RunConfigurationFlagsState blazeFlags;
   private final RunConfigurationFlagsState exeFlags;
   private final BlazeBinaryState blazeBinary;
+  private final BlazeRunOnDistributedExecutorState runOnDistributedExecutor;
 
-  public BlazeCommandRunConfigurationCommonState(String buildSystemName) {
+  public BlazeCommandRunConfigurationCommonState(BuildSystem buildSystem) {
     command = new BlazeCommandState();
-    blazeFlags = new RunConfigurationFlagsState(USER_BLAZE_FLAG_TAG, buildSystemName + " flags:");
+    blazeFlags = new RunConfigurationFlagsState(USER_BLAZE_FLAG_TAG, buildSystem + " flags:");
     exeFlags = new RunConfigurationFlagsState(USER_EXE_FLAG_TAG, "Executable flags:");
     blazeBinary = new BlazeBinaryState();
-    addStates(command, blazeFlags, exeFlags, blazeBinary);
+    runOnDistributedExecutor = new BlazeRunOnDistributedExecutorState(buildSystem);
+    addStates(command, blazeFlags, exeFlags, blazeBinary, runOnDistributedExecutor);
   }
 
   @Nullable
@@ -91,6 +96,14 @@
     return null;
   }
 
+  public Boolean getRunOnDistributedExecutor() {
+    return runOnDistributedExecutor.runOnDistributedExecutor;
+  }
+
+  public void setRunOnDistributedExecutor(Boolean runOnDistributedExecutor) {
+    this.runOnDistributedExecutor.runOnDistributedExecutor = runOnDistributedExecutor;
+  }
+
   public void validate(String buildSystemName) throws RuntimeConfigurationException {
     if (getCommand() == null) {
       throw new RuntimeConfigurationError("You must specify a command.");
@@ -100,4 +113,32 @@
       throw new RuntimeConfigurationError(buildSystemName + " binary does not exist");
     }
   }
+
+  @Override
+  public RunConfigurationStateEditor getEditor(Project project) {
+    return new RunConfigurationCompositeStateEditor(project, getStates()) {
+
+      @Nullable
+      private final RunOnExecutorStateEditor runOnExecutorEditor =
+          (RunOnExecutorStateEditor)
+              editors
+                  .stream()
+                  .filter(editor -> editor instanceof RunOnExecutorStateEditor)
+                  .findFirst()
+                  .orElse(null);
+
+      @Override
+      public void applyEditorTo(RunConfigurationState genericState) {
+        BlazeCommandRunConfigurationCommonState state =
+            (BlazeCommandRunConfigurationCommonState) genericState;
+        super.applyEditorTo(genericState);
+
+        // this editor needs to update based on state provided by other children.
+        if (runOnExecutorEditor != null) {
+          boolean isTest = BlazeCommandName.TEST.equals(state.getCommand());
+          runOnExecutorEditor.updateVisibility(isTest);
+        }
+      }
+    };
+  }
 }
diff --git a/base/src/com/google/idea/blaze/base/run/state/BlazeCommandState.java b/base/src/com/google/idea/blaze/base/run/state/BlazeCommandState.java
index 491acb7..221fe10 100644
--- a/base/src/com/google/idea/blaze/base/run/state/BlazeCommandState.java
+++ b/base/src/com/google/idea/blaze/base/run/state/BlazeCommandState.java
@@ -79,6 +79,11 @@
     }
 
     @Override
+    public void setComponentEnabled(boolean enabled) {
+      commandCombo.setEnabled(enabled);
+    }
+
+    @Override
     public void resetEditorFrom(RunConfigurationState genericState) {
       BlazeCommandState state = (BlazeCommandState) genericState;
       commandCombo.setSelectedItem(state.getCommand());
diff --git a/base/src/com/google/idea/blaze/base/run/state/BlazeRunOnDistributedExecutorState.java b/base/src/com/google/idea/blaze/base/run/state/BlazeRunOnDistributedExecutorState.java
new file mode 100644
index 0000000..b251d3f
--- /dev/null
+++ b/base/src/com/google/idea/blaze/base/run/state/BlazeRunOnDistributedExecutorState.java
@@ -0,0 +1,127 @@
+/*
+ * 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.run.state;
+
+import com.google.idea.blaze.base.run.DistributedExecutorSupport;
+import com.google.idea.blaze.base.settings.Blaze.BuildSystem;
+import com.google.idea.blaze.base.ui.UiUtil;
+import com.intellij.icons.AllIcons;
+import com.intellij.openapi.project.Project;
+import com.intellij.openapi.util.InvalidDataException;
+import com.intellij.openapi.util.WriteExternalException;
+import com.intellij.ui.components.JBCheckBox;
+import javax.annotation.Nullable;
+import javax.swing.JComponent;
+import javax.swing.JLabel;
+import org.jdom.Element;
+
+/**
+ * Provides an option to run blaze/bazel on a distributed executor, if available, rather than
+ * locally.
+ */
+public class BlazeRunOnDistributedExecutorState implements RunConfigurationState {
+
+  private static final String RUN_ON_DISTRIBUTED_EXECUTOR_ATTR =
+      "blaze-run-on-distributed-executor";
+
+  @Nullable private final DistributedExecutorSupport executorInfo;
+
+  public Boolean runOnDistributedExecutor = null;
+
+  BlazeRunOnDistributedExecutorState(BuildSystem buildSystem) {
+    this.executorInfo = DistributedExecutorSupport.getAvailableExecutor(buildSystem);
+  }
+
+  @Override
+  public void readExternal(Element element) throws InvalidDataException {
+    String string = element.getAttributeValue(RUN_ON_DISTRIBUTED_EXECUTOR_ATTR);
+    if (string != null) {
+      runOnDistributedExecutor = Boolean.parseBoolean(string);
+    }
+  }
+
+  @Override
+  public void writeExternal(Element element) throws WriteExternalException {
+    if (executorInfo != null && runOnDistributedExecutor != null) {
+      element.setAttribute(
+          RUN_ON_DISTRIBUTED_EXECUTOR_ATTR, Boolean.toString(runOnDistributedExecutor));
+    } else {
+      element.removeAttribute(RUN_ON_DISTRIBUTED_EXECUTOR_ATTR);
+    }
+  }
+
+  @Override
+  public RunOnExecutorStateEditor getEditor(Project project) {
+    return new RunOnExecutorStateEditor();
+  }
+
+  /** Editor for {@link BlazeRunOnDistributedExecutorState} */
+  class RunOnExecutorStateEditor implements RunConfigurationStateEditor {
+
+    private final JBCheckBox checkBox =
+        new JBCheckBox("Run on " + (executorInfo != null ? executorInfo.executorName() : null));
+    private final JLabel warning =
+        new JLabel(
+            "Warning: test UI integration is not available when running on distributed "
+                + "executor");
+
+    private boolean componentVisible = executorInfo != null;
+    private boolean isTest = false;
+
+    private RunOnExecutorStateEditor() {
+      warning.setIcon(AllIcons.RunConfigurations.ConfigurationWarning);
+      checkBox.addItemListener(e -> setVisibility());
+      setVisibility();
+    }
+
+    @Override
+    public void resetEditorFrom(RunConfigurationState genericState) {
+      BlazeRunOnDistributedExecutorState state = (BlazeRunOnDistributedExecutorState) genericState;
+      if (state.runOnDistributedExecutor != null) {
+        checkBox.setSelected(state.runOnDistributedExecutor);
+      }
+    }
+
+    @Override
+    public void applyEditorTo(RunConfigurationState genericState) {
+      BlazeRunOnDistributedExecutorState state = (BlazeRunOnDistributedExecutorState) genericState;
+      if (checkBox.isVisible()) {
+        state.runOnDistributedExecutor = checkBox.isSelected();
+      }
+    }
+
+    @Override
+    public JComponent createComponent() {
+      return UiUtil.createBox(checkBox, warning);
+    }
+
+    @Override
+    public void setComponentEnabled(boolean enabled) {
+      checkBox.setEnabled(enabled);
+    }
+
+    void updateVisibility(boolean isTest) {
+      this.componentVisible = executorInfo != null;
+      this.isTest = isTest;
+      setVisibility();
+    }
+
+    private void setVisibility() {
+      warning.setVisible(componentVisible && isTest && checkBox.isSelected());
+      checkBox.setVisible(componentVisible);
+    }
+  }
+}
diff --git a/base/src/com/google/idea/blaze/base/run/state/RunConfigurationCompositeState.java b/base/src/com/google/idea/blaze/base/run/state/RunConfigurationCompositeState.java
index 077e02d..a317232 100644
--- a/base/src/com/google/idea/blaze/base/run/state/RunConfigurationCompositeState.java
+++ b/base/src/com/google/idea/blaze/base/run/state/RunConfigurationCompositeState.java
@@ -15,6 +15,7 @@
  */
 package com.google.idea.blaze.base.run.state;
 
+import com.google.common.collect.ImmutableList;
 import com.google.common.collect.Lists;
 import com.google.idea.blaze.base.ui.UiUtil;
 import com.intellij.openapi.project.Project;
@@ -30,10 +31,6 @@
 public class RunConfigurationCompositeState implements RunConfigurationState {
   private final List<RunConfigurationState> states;
 
-  public RunConfigurationCompositeState(List<RunConfigurationState> states) {
-    this.states = states;
-  }
-
   protected RunConfigurationCompositeState() {
     this.states = Lists.newArrayList();
   }
@@ -42,6 +39,10 @@
     Collections.addAll(this.states, states);
   }
 
+  protected ImmutableList<RunConfigurationState> getStates() {
+    return ImmutableList.copyOf(states);
+  }
+
   @Override
   public final void readExternal(Element element) throws InvalidDataException {
     for (RunConfigurationState state : states) {
@@ -60,11 +61,11 @@
 
   /** @return A {@link RunConfigurationStateEditor} for this state. */
   @Override
-  public final RunConfigurationStateEditor getEditor(Project project) {
+  public RunConfigurationStateEditor getEditor(Project project) {
     return new RunConfigurationCompositeStateEditor(project, states);
   }
 
-  private static class RunConfigurationCompositeStateEditor implements RunConfigurationStateEditor {
+  static class RunConfigurationCompositeStateEditor implements RunConfigurationStateEditor {
     List<RunConfigurationStateEditor> editors;
 
     public RunConfigurationCompositeStateEditor(
@@ -96,5 +97,10 @@
               .map(RunConfigurationStateEditor::createComponent)
               .collect(Collectors.toList()));
     }
+
+    @Override
+    public void setComponentEnabled(boolean enabled) {
+      editors.forEach(editor -> editor.setComponentEnabled(enabled));
+    }
   }
 }
diff --git a/base/src/com/google/idea/blaze/base/run/state/RunConfigurationFlagsState.java b/base/src/com/google/idea/blaze/base/run/state/RunConfigurationFlagsState.java
index c0edaa4..87d306c 100644
--- a/base/src/com/google/idea/blaze/base/run/state/RunConfigurationFlagsState.java
+++ b/base/src/com/google/idea/blaze/base/run/state/RunConfigurationFlagsState.java
@@ -19,6 +19,7 @@
 import com.google.common.collect.ImmutableList;
 import com.google.idea.blaze.base.ui.UiUtil;
 import com.intellij.openapi.project.Project;
+import com.intellij.openapi.util.text.StringUtil;
 import com.intellij.util.execution.ParametersListUtil;
 import java.util.List;
 import javax.swing.JComponent;
@@ -85,27 +86,38 @@
       this.fieldLabel = fieldLabel;
     }
 
+    /** Identical to {@link ParametersListUtil#join}, except args are newline-delimited. */
     private static String makeFlagString(List<String> flags) {
-      StringBuilder flagString = new StringBuilder();
+      StringBuilder builder = new StringBuilder();
       for (String flag : flags) {
-        if (flagString.length() > 0) {
-          flagString.append('\n');
+        if (builder.length() > 0) {
+          builder.append('\n');
         }
-        if (flag.isEmpty() || flag.contains(" ") || flag.contains("|")) {
-          flagString.append('"');
-          flagString.append(flag);
-          flagString.append('"');
-        } else {
-          flagString.append(flag);
-        }
+        builder.append(encode(flag));
       }
-      return flagString.toString();
+      return builder.toString();
+    }
+
+    private static String encode(String flag) {
+      StringBuilder builder = new StringBuilder();
+      builder.append(flag);
+      StringUtil.escapeQuotes(builder);
+      if (builder.length() == 0
+          || StringUtil.indexOf(builder, ' ') >= 0
+          || StringUtil.indexOf(builder, '|') >= 0) {
+        StringUtil.quote(builder);
+      }
+      return builder.toString();
+    }
+
+    @Override
+    public void setComponentEnabled(boolean enabled) {
+      flagsField.setEnabled(enabled);
     }
 
     @Override
     public void resetEditorFrom(RunConfigurationState genericState) {
       RunConfigurationFlagsState state = (RunConfigurationFlagsState) genericState;
-      // Normally we could just use ParametersListUtils.join, but that will only space-delimit args.
       flagsField.setText(makeFlagString(state.getFlags()));
     }
 
diff --git a/base/src/com/google/idea/blaze/base/run/state/RunConfigurationStateEditor.java b/base/src/com/google/idea/blaze/base/run/state/RunConfigurationStateEditor.java
index b04020e..3a905fc 100644
--- a/base/src/com/google/idea/blaze/base/run/state/RunConfigurationStateEditor.java
+++ b/base/src/com/google/idea/blaze/base/run/state/RunConfigurationStateEditor.java
@@ -28,4 +28,6 @@
 
   /** @return A component to display for the editor. */
   JComponent createComponent();
+
+  void setComponentEnabled(boolean enabled);
 }
diff --git a/base/src/com/google/idea/blaze/base/run/testmap/TestTargetFilterImpl.java b/base/src/com/google/idea/blaze/base/run/testmap/TestTargetFilterImpl.java
index 0b336f4..99d490f 100644
--- a/base/src/com/google/idea/blaze/base/run/testmap/TestTargetFilterImpl.java
+++ b/base/src/com/google/idea/blaze/base/run/testmap/TestTargetFilterImpl.java
@@ -134,7 +134,8 @@
               Kind.JAVA_TEST,
               Kind.GWT_TEST,
               Kind.CC_TEST,
-              Kind.PY_TEST);
+              Kind.PY_TEST,
+              Kind.GO_TEST);
     }
   }
 
diff --git a/base/src/com/google/idea/blaze/base/settings/ui/EditProjectViewAction.java b/base/src/com/google/idea/blaze/base/settings/ui/EditProjectViewAction.java
index b2e86be..926483b 100644
--- a/base/src/com/google/idea/blaze/base/settings/ui/EditProjectViewAction.java
+++ b/base/src/com/google/idea/blaze/base/settings/ui/EditProjectViewAction.java
@@ -15,7 +15,7 @@
  */
 package com.google.idea.blaze.base.settings.ui;
 
-import com.google.idea.blaze.base.actions.BlazeAction;
+import com.google.idea.blaze.base.actions.BlazeProjectAction;
 import com.google.idea.blaze.base.projectview.ProjectViewManager;
 import com.google.idea.blaze.base.projectview.ProjectViewSet;
 import com.intellij.openapi.actionSystem.AnActionEvent;
@@ -27,14 +27,10 @@
 import java.io.File;
 
 /** Opens all the user's project views. */
-public class EditProjectViewAction extends BlazeAction {
+public class EditProjectViewAction extends BlazeProjectAction {
 
   @Override
-  public void actionPerformed(AnActionEvent e) {
-    Project project = e.getProject();
-    if (project == null) {
-      return;
-    }
+  protected void actionPerformedInBlazeProject(Project project, AnActionEvent e) {
     ProjectViewSet projectViewSet = ProjectViewManager.getInstance(project).getProjectViewSet();
     if (projectViewSet == null) {
       return;
diff --git a/base/src/com/google/idea/blaze/base/sync/BlazeSyncManager.java b/base/src/com/google/idea/blaze/base/sync/BlazeSyncManager.java
index db3cba2..c1c36b7 100644
--- a/base/src/com/google/idea/blaze/base/sync/BlazeSyncManager.java
+++ b/base/src/com/google/idea/blaze/base/sync/BlazeSyncManager.java
@@ -16,29 +16,31 @@
 package com.google.idea.blaze.base.sync;
 
 import com.google.idea.blaze.base.async.executor.BlazeExecutor;
+import com.google.idea.blaze.base.model.primitives.TargetExpression;
 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.settings.BlazeUserSettings;
 import com.intellij.openapi.components.ServiceManager;
 import com.intellij.openapi.project.Project;
 import com.intellij.openapi.startup.StartupManager;
-import org.jetbrains.annotations.NotNull;
+import java.util.List;
 
 /** Manages syncing and its listeners. */
 public class BlazeSyncManager {
 
-  @NotNull private final Project project;
+  private final Project project;
 
-  public BlazeSyncManager(@NotNull Project project) {
+  public BlazeSyncManager(Project project) {
     this.project = project;
   }
 
-  public static BlazeSyncManager getInstance(@NotNull Project project) {
+  public static BlazeSyncManager getInstance(Project project) {
     return ServiceManager.getService(project, BlazeSyncManager.class);
   }
 
   /** Requests a project sync with Blaze. */
-  public void requestProjectSync(@NotNull final BlazeSyncParams syncParams) {
+  public void requestProjectSync(final BlazeSyncParams syncParams) {
     StartupManager.getInstance(project)
         .runWhenProjectIsInitialized(
             new Runnable() {
@@ -59,4 +61,38 @@
               }
             });
   }
+
+  public void fullProjectSync() {
+    BlazeSyncParams syncParams =
+        new BlazeSyncParams.Builder("Full Sync", BlazeSyncParams.SyncMode.FULL)
+            .addProjectViewTargets(true)
+            .addWorkingSet(BlazeUserSettings.getInstance().getExpandSyncToWorkingSet())
+            .build();
+    requestProjectSync(syncParams);
+  }
+
+  public void incrementalProjectSync() {
+    BlazeSyncParams syncParams =
+        new BlazeSyncParams.Builder("Sync", BlazeSyncParams.SyncMode.INCREMENTAL)
+            .addProjectViewTargets(true)
+            .addWorkingSet(BlazeUserSettings.getInstance().getExpandSyncToWorkingSet())
+            .build();
+    requestProjectSync(syncParams);
+  }
+
+  public void partialSync(List<TargetExpression> targetExpressions) {
+    BlazeSyncParams syncParams =
+        new BlazeSyncParams.Builder("Partial Sync", BlazeSyncParams.SyncMode.PARTIAL)
+            .addTargetExpressions(targetExpressions)
+            .build();
+    requestProjectSync(syncParams);
+  }
+
+  public void workingSetSync() {
+    BlazeSyncParams syncParams =
+        new BlazeSyncParams.Builder("Sync Working Set", BlazeSyncParams.SyncMode.PARTIAL)
+            .addWorkingSet(true)
+            .build();
+    requestProjectSync(syncParams);
+  }
 }
diff --git a/base/src/com/google/idea/blaze/base/sync/BlazeSyncParams.java b/base/src/com/google/idea/blaze/base/sync/BlazeSyncParams.java
index 2ee84c7..22fba9c 100644
--- a/base/src/com/google/idea/blaze/base/sync/BlazeSyncParams.java
+++ b/base/src/com/google/idea/blaze/base/sync/BlazeSyncParams.java
@@ -27,7 +27,7 @@
   /** The kind of sync. */
   public enum SyncMode {
     /** Happens on startup, restores in-memory state */
-    RESTORE_EPHEMERAL_STATE,
+    STARTUP,
     /** Partial / working set sync */
     PARTIAL,
     /** This is the standard incremental sync */
diff --git a/base/src/com/google/idea/blaze/base/sync/BlazeSyncPlugin.java b/base/src/com/google/idea/blaze/base/sync/BlazeSyncPlugin.java
index 0b55343..aa429f3 100644
--- a/base/src/com/google/idea/blaze/base/sync/BlazeSyncPlugin.java
+++ b/base/src/com/google/idea/blaze/base/sync/BlazeSyncPlugin.java
@@ -19,6 +19,7 @@
 import com.google.common.collect.ImmutableSet;
 import com.google.idea.blaze.base.ideinfo.TargetMap;
 import com.google.idea.blaze.base.model.BlazeProjectData;
+import com.google.idea.blaze.base.model.BlazeVersionData;
 import com.google.idea.blaze.base.model.SyncState;
 import com.google.idea.blaze.base.model.primitives.LanguageClass;
 import com.google.idea.blaze.base.model.primitives.WorkspaceRoot;
@@ -119,6 +120,7 @@
       Project project,
       BlazeContext context,
       ProjectViewSet projectViewSet,
+      BlazeVersionData blazeVersionData,
       BlazeProjectData blazeProjectData);
 
   @Nullable
@@ -136,6 +138,19 @@
       Module workspaceModule,
       ModifiableRootModel workspaceModifiableModel);
 
+  /**
+   * Updates in-memory state that isn't serialized by IntelliJ.
+   *
+   * <p>Called on sync and on startup, after updateProjectStructure. May not do any write actions.
+   */
+  void updateInMemoryState(
+      Project project,
+      BlazeContext context,
+      WorkspaceRoot workspaceRoot,
+      ProjectViewSet projectViewSet,
+      BlazeProjectData blazeProjectData,
+      Module workspaceModule);
+
   /** Validates the project. */
   boolean validate(Project project, BlazeContext context, BlazeProjectData blazeProjectData);
 
@@ -203,6 +218,7 @@
         Project project,
         BlazeContext context,
         ProjectViewSet projectViewSet,
+        BlazeVersionData blazeVersionData,
         BlazeProjectData blazeProjectData) {}
 
     @Nullable
@@ -224,6 +240,15 @@
         ModifiableRootModel workspaceModifiableModel) {}
 
     @Override
+    public void updateInMemoryState(
+        Project project,
+        BlazeContext context,
+        WorkspaceRoot workspaceRoot,
+        ProjectViewSet projectViewSet,
+        BlazeProjectData blazeProjectData,
+        Module workspaceModule) {}
+
+    @Override
     public boolean validate(
         Project project, BlazeContext context, BlazeProjectData blazeProjectData) {
       return true;
diff --git a/base/src/com/google/idea/blaze/base/sync/BlazeSyncStartupActivity.java b/base/src/com/google/idea/blaze/base/sync/BlazeSyncStartupActivity.java
index 5b6a8ee..0fe7dac 100644
--- a/base/src/com/google/idea/blaze/base/sync/BlazeSyncStartupActivity.java
+++ b/base/src/com/google/idea/blaze/base/sync/BlazeSyncStartupActivity.java
@@ -43,8 +43,7 @@
           .addWorkingSet(BlazeUserSettings.getInstance().getExpandSyncToWorkingSet())
           .build();
     }
-    return new BlazeSyncParams.Builder(
-            "Sync Project", BlazeSyncParams.SyncMode.RESTORE_EPHEMERAL_STATE)
+    return new BlazeSyncParams.Builder("Sync Project", BlazeSyncParams.SyncMode.STARTUP)
         .addProjectViewTargets(true)
         .addWorkingSet(BlazeUserSettings.getInstance().getExpandSyncToWorkingSet())
         .build();
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 3d4e3ec..1fb59c9 100644
--- a/base/src/com/google/idea/blaze/base/sync/BlazeSyncTask.java
+++ b/base/src/com/google/idea/blaze/base/sync/BlazeSyncTask.java
@@ -24,7 +24,6 @@
 import com.google.common.collect.Sets;
 import com.google.common.util.concurrent.ListenableFuture;
 import com.google.common.util.concurrent.ListeningExecutorService;
-import com.google.idea.blaze.base.async.AsyncUtil;
 import com.google.idea.blaze.base.async.FutureUtil;
 import com.google.idea.blaze.base.async.executor.BlazeExecutor;
 import com.google.idea.blaze.base.command.info.BlazeInfo;
@@ -32,10 +31,13 @@
 import com.google.idea.blaze.base.filecache.FileCaches;
 import com.google.idea.blaze.base.ideinfo.TargetKey;
 import com.google.idea.blaze.base.ideinfo.TargetMap;
+import com.google.idea.blaze.base.io.FileAttributeProvider;
 import com.google.idea.blaze.base.metrics.Action;
 import com.google.idea.blaze.base.model.BlazeLibrary;
 import com.google.idea.blaze.base.model.BlazeProjectData;
+import com.google.idea.blaze.base.model.BlazeVersionData;
 import com.google.idea.blaze.base.model.SyncState;
+import com.google.idea.blaze.base.model.SyncState.Builder;
 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;
@@ -66,6 +68,7 @@
 import com.google.idea.blaze.base.sync.aspects.BlazeIdeInterface;
 import com.google.idea.blaze.base.sync.aspects.BlazeIdeInterface.BuildResult;
 import com.google.idea.blaze.base.sync.data.BlazeDataStorage;
+import com.google.idea.blaze.base.sync.data.BlazeProjectDataManager;
 import com.google.idea.blaze.base.sync.data.BlazeProjectDataManagerImpl;
 import com.google.idea.blaze.base.sync.libraries.BlazeLibraryCollector;
 import com.google.idea.blaze.base.sync.libraries.LibraryEditor;
@@ -84,9 +87,11 @@
 import com.google.idea.blaze.base.targetmaps.ReverseDependencyMap;
 import com.google.idea.blaze.base.util.SaveUtil;
 import com.google.idea.blaze.base.vcs.BlazeVcsHandler;
+import com.google.idea.sdkcompat.transactions.Transactions;
 import com.intellij.openapi.application.ApplicationManager;
 import com.intellij.openapi.diagnostic.Logger;
 import com.intellij.openapi.module.Module;
+import com.intellij.openapi.module.ModuleManager;
 import com.intellij.openapi.module.ModuleType;
 import com.intellij.openapi.progress.ProgressIndicator;
 import com.intellij.openapi.progress.Progressive;
@@ -173,7 +178,18 @@
       }
 
       onSyncStart(project, context, syncMode);
-      syncResult = doSyncProject(context, syncMode, oldBlazeProjectData);
+      if (syncMode != SyncMode.STARTUP) {
+        syncResult = doSyncProject(context, syncMode, oldBlazeProjectData);
+      } else {
+        syncResult = SyncResult.SUCCESS;
+      }
+      if (syncResult.successful()) {
+        ProjectViewSet projectViewSet = ProjectViewManager.getInstance(project).getProjectViewSet();
+        BlazeProjectData blazeProjectData =
+            BlazeProjectDataManager.getInstance(project).getBlazeProjectData();
+        updateInMemoryState(project, context, projectViewSet, blazeProjectData);
+        onSyncComplete(project, context, projectViewSet, blazeProjectData, syncMode, syncResult);
+      }
     } catch (AssertionError | Exception e) {
       LOG.error(e);
       IssueOutput.error("Internal error: " + e.getMessage()).submit(context);
@@ -188,13 +204,13 @@
       BlazeContext context, SyncMode syncMode, @Nullable BlazeProjectData oldBlazeProjectData) {
     this.syncStartTime = System.currentTimeMillis();
 
-    BlazeVcsHandler vcsHandler = null;
-    for (BlazeVcsHandler candidate : BlazeVcsHandler.EP_NAME.getExtensions()) {
-      if (candidate.handlesProject(importSettings.getBuildSystem(), workspaceRoot)) {
-        vcsHandler = candidate;
-        break;
-      }
+    if (!FileAttributeProvider.getInstance().exists(workspaceRoot.directory())) {
+      IssueOutput.error(String.format("Workspace '%s' doesn't exist.", workspaceRoot.directory()))
+          .submit(context);
+      return SyncResult.FAILURE;
     }
+
+    BlazeVcsHandler vcsHandler = BlazeVcsHandler.vcsHandlerForProject(project);
     if (vcsHandler == null) {
       IssueOutput.error("Could not find a VCS handler").submit(context);
       return SyncResult.FAILURE;
@@ -222,6 +238,8 @@
     }
     BlazeRoots blazeRoots =
         BlazeRoots.build(importSettings.getBuildSystem(), workspaceRoot, blazeInfo);
+    BlazeVersionData blazeVersionData =
+        BlazeVersionData.build(importSettings.getBuildSystem(), workspaceRoot, blazeInfo);
 
     WorkspacePathResolverAndProjectView workspacePathResolverAndProjectView =
         computeWorkspacePathResolverAndProjectView(context, blazeRoots, vcsHandler, executor);
@@ -245,7 +263,7 @@
     }
 
     if (!ProjectViewVerifier.verifyProjectView(
-        context, workspaceRoot, projectViewSet, workspaceLanguageSettings)) {
+        context, workspacePathResolver, projectViewSet, workspaceLanguageSettings)) {
       return SyncResult.FAILURE;
     }
 
@@ -269,120 +287,112 @@
       printWorkingSet(context, workingSet);
     }
 
-    BuildResult ideInfoResult = BuildResult.SUCCESS;
-    BuildResult ideResolveResult = BuildResult.SUCCESS;
-    if (syncMode != SyncMode.RESTORE_EPHEMERAL_STATE || oldBlazeProjectData == null) {
-      SyncState.Builder syncStateBuilder = new SyncState.Builder();
-      SyncState previousSyncState =
-          oldBlazeProjectData != null ? oldBlazeProjectData.syncState : null;
+    SyncState.Builder syncStateBuilder = new SyncState.Builder();
+    SyncState previousSyncState =
+        oldBlazeProjectData != null ? oldBlazeProjectData.syncState : null;
 
-      List<TargetExpression> targets = Lists.newArrayList();
-      if (syncParams.addProjectViewTargets || oldBlazeProjectData == null) {
-        Collection<TargetExpression> projectViewTargets =
-            projectViewSet.listItems(TargetSection.KEY);
-        if (!projectViewTargets.isEmpty()) {
-          targets.addAll(projectViewTargets);
-          printTargets(context, "project view", projectViewTargets);
-        }
+    List<TargetExpression> targets = Lists.newArrayList();
+    if (syncParams.addProjectViewTargets || oldBlazeProjectData == null) {
+      Collection<TargetExpression> projectViewTargets = projectViewSet.listItems(TargetSection.KEY);
+      if (!projectViewTargets.isEmpty()) {
+        targets.addAll(projectViewTargets);
+        printTargets(context, "project view", projectViewTargets);
       }
-      if (syncParams.addWorkingSet && workingSet != null) {
-        Collection<? extends TargetExpression> workingSetTargets =
-            getWorkingSetTargets(projectViewSet, workingSet);
-        if (!workingSetTargets.isEmpty()) {
-          targets.addAll(workingSetTargets);
-          printTargets(context, "working set", workingSetTargets);
-        }
-      }
-      if (!syncParams.targetExpressions.isEmpty()) {
-        targets.addAll(syncParams.targetExpressions);
-        printTargets(context, syncParams.title, syncParams.targetExpressions);
-      }
-
-      boolean mergeWithOldState = !syncParams.addProjectViewTargets;
-      BlazeIdeInterface.IdeResult ideQueryResult =
-          getIdeQueryResult(
-              project,
-              context,
-              projectViewSet,
-              targets,
-              workspaceLanguageSettings,
-              artifactLocationDecoder,
-              syncStateBuilder,
-              previousSyncState,
-              mergeWithOldState);
-      if (context.isCancelled()) {
-        return SyncResult.CANCELLED;
-      }
-      if (ideQueryResult.targetMap == null
-          || ideQueryResult.buildResult == BuildResult.FATAL_ERROR) {
-        context.setHasError();
-        return SyncResult.FAILURE;
-      }
-
-      TargetMap targetMap = ideQueryResult.targetMap;
-      ideInfoResult = ideQueryResult.buildResult;
-
-      ListenableFuture<ImmutableMultimap<TargetKey, TargetKey>> reverseDependenciesFuture =
-          BlazeExecutor.getInstance().submit(() -> ReverseDependencyMap.createRdepsMap(targetMap));
-
-      ideResolveResult =
-          resolveIdeArtifacts(project, context, workspaceRoot, projectViewSet, targets);
-      if (ideResolveResult == BuildResult.FATAL_ERROR) {
-        context.setHasError();
-        return SyncResult.FAILURE;
-      }
-      if (context.isCancelled()) {
-        return SyncResult.CANCELLED;
-      }
-
-      Scope.push(
-          context,
-          (childContext) -> {
-            childContext.push(new TimingScope("UpdateSyncState"));
-            for (BlazeSyncPlugin syncPlugin : BlazeSyncPlugin.EP_NAME.getExtensions()) {
-              syncPlugin.updateSyncState(
-                  project,
-                  childContext,
-                  workspaceRoot,
-                  projectViewSet,
-                  workspaceLanguageSettings,
-                  blazeRoots,
-                  workingSet,
-                  workspacePathResolver,
-                  artifactLocationDecoder,
-                  targetMap,
-                  syncStateBuilder,
-                  previousSyncState);
-            }
-          });
-
-      ImmutableMultimap<TargetKey, TargetKey> reverseDependencies =
-          FutureUtil.waitForFuture(context, reverseDependenciesFuture)
-              .timed("ReverseDependencies")
-              .onError("Failed to compute reverse dependency map")
-              .run()
-              .result();
-      if (reverseDependencies == null) {
-        return SyncResult.FAILURE;
-      }
-
-      newBlazeProjectData =
-          new BlazeProjectData(
-              syncStartTime,
-              targetMap,
-              blazeInfo,
-              blazeRoots,
-              workingSet,
-              workspacePathResolver,
-              artifactLocationDecoder,
-              workspaceLanguageSettings,
-              syncStateBuilder.build(),
-              reverseDependencies,
-              vcsHandler.getVcsName());
-    } else {
-      // Restore project based on old blaze project data
-      newBlazeProjectData = oldBlazeProjectData;
     }
+    if (syncParams.addWorkingSet && workingSet != null) {
+      Collection<? extends TargetExpression> workingSetTargets =
+          getWorkingSetTargets(projectViewSet, workingSet);
+      if (!workingSetTargets.isEmpty()) {
+        targets.addAll(workingSetTargets);
+        printTargets(context, "working set", workingSetTargets);
+      }
+    }
+    if (!syncParams.targetExpressions.isEmpty()) {
+      targets.addAll(syncParams.targetExpressions);
+      printTargets(context, syncParams.title, syncParams.targetExpressions);
+    }
+
+    boolean mergeWithOldState = !syncParams.addProjectViewTargets;
+    BlazeIdeInterface.IdeResult ideQueryResult =
+        getIdeQueryResult(
+            project,
+            context,
+            projectViewSet,
+            blazeVersionData,
+            targets,
+            workspaceLanguageSettings,
+            artifactLocationDecoder,
+            syncStateBuilder,
+            previousSyncState,
+            mergeWithOldState);
+    if (context.isCancelled()) {
+      return SyncResult.CANCELLED;
+    }
+    if (ideQueryResult.targetMap == null || ideQueryResult.buildResult == BuildResult.FATAL_ERROR) {
+      context.setHasError();
+      return SyncResult.FAILURE;
+    }
+
+    TargetMap targetMap = ideQueryResult.targetMap;
+    BuildResult ideInfoResult = ideQueryResult.buildResult;
+
+    ListenableFuture<ImmutableMultimap<TargetKey, TargetKey>> reverseDependenciesFuture =
+        BlazeExecutor.getInstance().submit(() -> ReverseDependencyMap.createRdepsMap(targetMap));
+
+    BuildResult ideResolveResult =
+        resolveIdeArtifacts(
+            project, context, workspaceRoot, projectViewSet, blazeVersionData, targets);
+    if (ideResolveResult == BuildResult.FATAL_ERROR) {
+      context.setHasError();
+      return SyncResult.FAILURE;
+    }
+    if (context.isCancelled()) {
+      return SyncResult.CANCELLED;
+    }
+
+    Scope.push(
+        context,
+        (childContext) -> {
+          childContext.push(new TimingScope("UpdateSyncState"));
+          for (BlazeSyncPlugin syncPlugin : BlazeSyncPlugin.EP_NAME.getExtensions()) {
+            syncPlugin.updateSyncState(
+                project,
+                childContext,
+                workspaceRoot,
+                projectViewSet,
+                workspaceLanguageSettings,
+                blazeRoots,
+                workingSet,
+                workspacePathResolver,
+                artifactLocationDecoder,
+                targetMap,
+                syncStateBuilder,
+                previousSyncState);
+          }
+        });
+
+    ImmutableMultimap<TargetKey, TargetKey> reverseDependencies =
+        FutureUtil.waitForFuture(context, reverseDependenciesFuture)
+            .timed("ReverseDependencies")
+            .onError("Failed to compute reverse dependency map")
+            .run()
+            .result();
+    if (reverseDependencies == null) {
+      return SyncResult.FAILURE;
+    }
+
+    newBlazeProjectData =
+        new BlazeProjectData(
+            syncStartTime,
+            targetMap,
+            blazeInfo,
+            blazeRoots,
+            blazeVersionData,
+            workspacePathResolver,
+            artifactLocationDecoder,
+            workspaceLanguageSettings,
+            syncStateBuilder.build(),
+            reverseDependencies);
 
     FileCaches.onSync(project, context, projectViewSet, newBlazeProjectData, syncMode);
     ListenableFuture<?> prefetch =
@@ -394,7 +404,13 @@
         .run();
 
     boolean success =
-        updateProject(project, context, projectViewSet, oldBlazeProjectData, newBlazeProjectData);
+        updateProject(
+            project,
+            context,
+            projectViewSet,
+            blazeVersionData,
+            oldBlazeProjectData,
+            newBlazeProjectData);
     if (!success) {
       return SyncResult.FAILURE;
     }
@@ -417,7 +433,6 @@
       syncResult = SyncResult.PARTIAL_SUCCESS;
     }
 
-    onSyncComplete(project, context, projectViewSet, newBlazeProjectData, syncMode, syncResult);
     return syncResult;
   }
 
@@ -555,10 +570,11 @@
       Project project,
       BlazeContext parentContext,
       ProjectViewSet projectViewSet,
+      BlazeVersionData blazeVersionData,
       List<TargetExpression> targets,
       WorkspaceLanguageSettings workspaceLanguageSettings,
       ArtifactLocationDecoder artifactLocationDecoder,
-      SyncState.Builder syncStateBuilder,
+      Builder syncStateBuilder,
       @Nullable SyncState previousSyncState,
       boolean mergeWithOldState) {
 
@@ -575,6 +591,7 @@
               context,
               workspaceRoot,
               projectViewSet,
+              blazeVersionData,
               targets,
               workspaceLanguageSettings,
               artifactLocationDecoder,
@@ -589,6 +606,7 @@
       BlazeContext parentContext,
       WorkspaceRoot workspaceRoot,
       ProjectViewSet projectViewSet,
+      BlazeVersionData blazeVersionData,
       List<TargetExpression> targetExpressions) {
     return Scope.push(
         parentContext,
@@ -606,7 +624,7 @@
           }
           BlazeIdeInterface blazeIdeInterface = BlazeIdeInterface.getInstance();
           return blazeIdeInterface.resolveIdeArtifacts(
-              project, context, workspaceRoot, projectViewSet, targetExpressions);
+              project, context, workspaceRoot, projectViewSet, blazeVersionData, targetExpressions);
         });
   }
 
@@ -614,6 +632,7 @@
       Project project,
       BlazeContext parentContext,
       ProjectViewSet projectViewSet,
+      BlazeVersionData blazeVersionData,
       @Nullable BlazeProjectData oldBlazeProjectData,
       BlazeProjectData newBlazeProjectData) {
     return Scope.push(
@@ -625,19 +644,29 @@
           context.output(new StatusOutput("Committing project structure..."));
 
           try {
-            AsyncUtil.executeProjectChangeAction(
-                () ->
-                    ProjectRootManagerEx.getInstanceEx(this.project)
-                        .mergeRootsChangesDuring(
-                            () -> {
-                              updateProjectSdk(context, projectViewSet, newBlazeProjectData);
-                              updateProjectStructure(
-                                  context,
-                                  importSettings,
-                                  projectViewSet,
-                                  newBlazeProjectData,
-                                  oldBlazeProjectData);
-                            }));
+            Transactions.submitTransactionAndWait(
+                () -> {
+                  ApplicationManager.getApplication()
+                      .runWriteAction(
+                          (Runnable)
+                              () -> {
+                                ProjectRootManagerEx.getInstanceEx(this.project)
+                                    .mergeRootsChangesDuring(
+                                        () -> {
+                                          updateProjectSdk(
+                                              context,
+                                              projectViewSet,
+                                              blazeVersionData,
+                                              newBlazeProjectData);
+                                          updateProjectStructure(
+                                              context,
+                                              importSettings,
+                                              projectViewSet,
+                                              newBlazeProjectData,
+                                              oldBlazeProjectData);
+                                        });
+                              });
+                });
           } catch (Throwable t) {
             IssueOutput.error("Internal error. Error: " + t).submit(context);
             LOG.error(t);
@@ -651,9 +680,13 @@
   }
 
   private void updateProjectSdk(
-      BlazeContext context, ProjectViewSet projectViewSet, BlazeProjectData newBlazeProjectData) {
+      BlazeContext context,
+      ProjectViewSet projectViewSet,
+      BlazeVersionData blazeVersionData,
+      BlazeProjectData newBlazeProjectData) {
     for (BlazeSyncPlugin syncPlugin : BlazeSyncPlugin.EP_NAME.getExtensions()) {
-      syncPlugin.updateProjectSdk(project, context, projectViewSet, newBlazeProjectData);
+      syncPlugin.updateProjectSdk(
+          project, context, projectViewSet, blazeVersionData, newBlazeProjectData);
     }
   }
 
@@ -716,6 +749,36 @@
     moduleEditor.commitWithGc(context);
   }
 
+  private void updateInMemoryState(
+      Project project,
+      BlazeContext parentContext,
+      ProjectViewSet projectViewSet,
+      BlazeProjectData blazeProjectData) {
+    Scope.push(
+        parentContext,
+        context -> {
+          context.push(new TimingScope("UpdateInMemoryState"));
+          context.output(new StatusOutput("Updating in-memory state..."));
+          ApplicationManager.getApplication()
+              .runReadAction(
+                  () -> {
+                    Module workspaceModule =
+                        ModuleManager.getInstance(project)
+                            .findModuleByName(BlazeDataStorage.WORKSPACE_MODULE_NAME);
+                    for (BlazeSyncPlugin blazeSyncPlugin :
+                        BlazeSyncPlugin.EP_NAME.getExtensions()) {
+                      blazeSyncPlugin.updateInMemoryState(
+                          project,
+                          context,
+                          workspaceRoot,
+                          projectViewSet,
+                          blazeProjectData,
+                          workspaceModule);
+                    }
+                  });
+        });
+  }
+
   /**
    * Creates a module that includes the user's data directory.
    *
diff --git a/base/src/com/google/idea/blaze/base/sync/SyncListener.java b/base/src/com/google/idea/blaze/base/sync/SyncListener.java
index a3512cb..7a0cf03 100644
--- a/base/src/com/google/idea/blaze/base/sync/SyncListener.java
+++ b/base/src/com/google/idea/blaze/base/sync/SyncListener.java
@@ -31,13 +31,23 @@
   /** Result of the sync operation */
   enum SyncResult {
     /** Full success */
-    SUCCESS,
+    SUCCESS(true),
     /** The user has errors in their BUILD files or compilation errors */
-    PARTIAL_SUCCESS,
+    PARTIAL_SUCCESS(true),
     /** The user cancelled */
-    CANCELLED,
+    CANCELLED(false),
     /** Failure -- sync could not complete */
-    FAILURE,
+    FAILURE(false);
+
+    private final boolean success;
+
+    SyncResult(boolean success) {
+      this.success = success;
+    }
+
+    public boolean successful() {
+      return success;
+    }
   }
 
   /** Called after open documents have been saved, prior to starting the blaze sync. */
diff --git a/base/src/com/google/idea/blaze/base/sync/actions/ExpandSyncToWorkingSetAction.java b/base/src/com/google/idea/blaze/base/sync/actions/ExpandSyncToWorkingSetAction.java
index 0509cca..54f2ca1 100644
--- a/base/src/com/google/idea/blaze/base/sync/actions/ExpandSyncToWorkingSetAction.java
+++ b/base/src/com/google/idea/blaze/base/sync/actions/ExpandSyncToWorkingSetAction.java
@@ -15,12 +15,12 @@
  */
 package com.google.idea.blaze.base.sync.actions;
 
-import com.google.idea.blaze.base.actions.BlazeToggleAction;
+import com.google.idea.blaze.base.actions.BlazeProjectToggleAction;
 import com.google.idea.blaze.base.settings.BlazeUserSettings;
 import com.intellij.openapi.actionSystem.AnActionEvent;
 
 /** Manages a tick box of whether to expand the sync targets to the working set. */
-public class ExpandSyncToWorkingSetAction extends BlazeToggleAction {
+public class ExpandSyncToWorkingSetAction extends BlazeProjectToggleAction {
   @Override
   public boolean isSelected(AnActionEvent e) {
     return BlazeUserSettings.getInstance().getExpandSyncToWorkingSet();
diff --git a/base/src/com/google/idea/blaze/base/sync/actions/FullSyncProjectAction.java b/base/src/com/google/idea/blaze/base/sync/actions/FullSyncProjectAction.java
index 2c1f625..dfe752e 100644
--- a/base/src/com/google/idea/blaze/base/sync/actions/FullSyncProjectAction.java
+++ b/base/src/com/google/idea/blaze/base/sync/actions/FullSyncProjectAction.java
@@ -15,38 +15,29 @@
  */
 package com.google.idea.blaze.base.sync.actions;
 
-import com.google.idea.blaze.base.actions.BlazeAction;
-import com.google.idea.blaze.base.settings.BlazeUserSettings;
+import com.google.idea.blaze.base.actions.BlazeProjectAction;
 import com.google.idea.blaze.base.sync.BlazeSyncManager;
-import com.google.idea.blaze.base.sync.BlazeSyncParams;
-import com.google.idea.blaze.base.sync.BlazeSyncParams.SyncMode;
+import com.google.idea.blaze.base.sync.status.BlazeSyncStatus;
 import com.intellij.openapi.actionSystem.AnActionEvent;
 import com.intellij.openapi.actionSystem.Presentation;
 import com.intellij.openapi.project.Project;
 
-/** Re-imports (syncs) an Android-Blaze project, without showing the "Import Project" wizard. */
-public class FullSyncProjectAction extends BlazeAction {
+/** Performs a full sync. This is like an incremental sync with some additional invalidation. */
+public class FullSyncProjectAction extends BlazeProjectAction {
 
-  public FullSyncProjectAction() {
-    super("Non-Incrementally Sync Project with BUILD Files");
+  @Override
+  protected void actionPerformedInBlazeProject(Project project, AnActionEvent e) {
+    BlazeSyncManager.getInstance(project).fullProjectSync();
+    updateStatus(project, e);
   }
 
   @Override
-  public void actionPerformed(final AnActionEvent e) {
-    Project project = e.getProject();
-    if (project != null) {
-      Presentation presentation = e.getPresentation();
-      presentation.setEnabled(false);
-      try {
-        BlazeSyncParams syncParams =
-            new BlazeSyncParams.Builder("Full Sync", SyncMode.FULL)
-                .addProjectViewTargets(true)
-                .addWorkingSet(BlazeUserSettings.getInstance().getExpandSyncToWorkingSet())
-                .build();
-        BlazeSyncManager.getInstance(project).requestProjectSync(syncParams);
-      } finally {
-        presentation.setEnabled(true);
-      }
-    }
+  protected void updateForBlazeProject(Project project, AnActionEvent e) {
+    updateStatus(project, e);
+  }
+
+  private static void updateStatus(Project project, AnActionEvent e) {
+    Presentation presentation = e.getPresentation();
+    presentation.setEnabled(!BlazeSyncStatus.getInstance(project).syncInProgress());
   }
 }
diff --git a/base/src/com/google/idea/blaze/base/sync/actions/IncrementalSyncProjectAction.java b/base/src/com/google/idea/blaze/base/sync/actions/IncrementalSyncProjectAction.java
index 134acfb..f772b67 100644
--- a/base/src/com/google/idea/blaze/base/sync/actions/IncrementalSyncProjectAction.java
+++ b/base/src/com/google/idea/blaze/base/sync/actions/IncrementalSyncProjectAction.java
@@ -15,14 +15,11 @@
  */
 package com.google.idea.blaze.base.sync.actions;
 
-import com.google.idea.blaze.base.actions.BlazeAction;
+import com.google.idea.blaze.base.actions.BlazeProjectAction;
 import com.google.idea.blaze.base.settings.Blaze;
 import com.google.idea.blaze.base.settings.BlazeUserSettings;
 import com.google.idea.blaze.base.sync.BlazeSyncManager;
-import com.google.idea.blaze.base.sync.BlazeSyncParams;
-import com.google.idea.blaze.base.sync.BlazeSyncParams.SyncMode;
 import com.google.idea.blaze.base.sync.status.BlazeSyncStatus;
-import com.google.idea.blaze.base.sync.status.BlazeSyncStatusImpl;
 import com.intellij.notification.Notification;
 import com.intellij.notification.NotificationDisplayType;
 import com.intellij.notification.NotificationGroup;
@@ -34,45 +31,27 @@
 import icons.BlazeIcons;
 import javax.swing.Icon;
 
-/** Re-imports (syncs) an Android-Blaze project, without showing the "Import Project" wizard. */
-public class IncrementalSyncProjectAction extends BlazeAction {
+/** Syncs the project with BUILD files. */
+public class IncrementalSyncProjectAction extends BlazeProjectAction {
 
-  public IncrementalSyncProjectAction() {
-    super("Sync Project with BUILD Files");
+  @Override
+  protected void actionPerformedInBlazeProject(Project project, AnActionEvent e) {
+    BlazeSyncManager.getInstance(project).incrementalProjectSync();
+    updateIcon(e);
   }
 
   @Override
-  public void actionPerformed(final AnActionEvent e) {
-    Project project = e.getProject();
-    if (project != null) {
-      BlazeSyncManager.getInstance(project)
-          .requestProjectSync(
-              new BlazeSyncParams.Builder("Sync", SyncMode.INCREMENTAL)
-                  .addProjectViewTargets(true)
-                  .addWorkingSet(BlazeUserSettings.getInstance().getExpandSyncToWorkingSet())
-                  .build());
-      updateIcon(e);
-    }
-  }
-
-  @Override
-  protected void doUpdate(AnActionEvent e) {
-    super.doUpdate(e);
+  protected void updateForBlazeProject(Project project, AnActionEvent e) {
     updateIcon(e);
   }
 
   private static void updateIcon(AnActionEvent e) {
     Project project = e.getProject();
     Presentation presentation = e.getPresentation();
-    if (project == null) {
-      presentation.setIcon(BlazeIcons.Blaze);
-      presentation.setEnabled(true);
-      return;
-    }
-    BlazeSyncStatusImpl statusHelper = BlazeSyncStatusImpl.getImpl(project);
+    BlazeSyncStatus statusHelper = BlazeSyncStatus.getInstance(project);
     BlazeSyncStatus.SyncStatus status = statusHelper.getStatus();
     presentation.setIcon(getIcon(status));
-    presentation.setEnabled(!statusHelper.syncInProgress.get());
+    presentation.setEnabled(!statusHelper.syncInProgress());
 
     if (status == BlazeSyncStatus.SyncStatus.DIRTY
         && !BlazeUserSettings.getInstance().getSyncStatusPopupShown()) {
diff --git a/base/src/com/google/idea/blaze/base/sync/actions/PartialSyncAction.java b/base/src/com/google/idea/blaze/base/sync/actions/PartialSyncAction.java
index e761f11..60e8484 100644
--- a/base/src/com/google/idea/blaze/base/sync/actions/PartialSyncAction.java
+++ b/base/src/com/google/idea/blaze/base/sync/actions/PartialSyncAction.java
@@ -15,8 +15,9 @@
  */
 package com.google.idea.blaze.base.sync.actions;
 
+import com.google.common.collect.ImmutableList;
 import com.google.common.collect.Lists;
-import com.google.idea.blaze.base.actions.BlazeAction;
+import com.google.idea.blaze.base.actions.BlazeProjectAction;
 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;
@@ -24,13 +25,13 @@
 import com.google.idea.blaze.base.settings.Blaze;
 import com.google.idea.blaze.base.settings.Blaze.BuildSystem;
 import com.google.idea.blaze.base.sync.BlazeSyncManager;
-import com.google.idea.blaze.base.sync.BlazeSyncParams;
 import com.google.idea.blaze.base.sync.BuildTargetFinder;
 import com.google.idea.blaze.base.sync.projectview.ImportRoots;
+import com.google.idea.blaze.base.sync.status.BlazeSyncStatus;
 import com.google.idea.blaze.base.targetmaps.SourceToTargetMap;
+import com.google.idea.common.actionhelper.ActionPresentationHelper;
 import com.intellij.openapi.actionSystem.AnActionEvent;
 import com.intellij.openapi.actionSystem.CommonDataKeys;
-import com.intellij.openapi.actionSystem.Presentation;
 import com.intellij.openapi.project.Project;
 import com.intellij.openapi.vfs.VirtualFile;
 import java.io.File;
@@ -38,57 +39,43 @@
 import javax.annotation.Nullable;
 
 /** Allows a partial sync of the project depending on what's been selected. */
-public class PartialSyncAction extends BlazeAction {
+public class PartialSyncAction extends BlazeProjectAction {
+
   @Override
-  public void actionPerformed(AnActionEvent e) {
-    Project project = e.getProject();
-    if (project != null) {
-      List<TargetExpression> targetExpressions = Lists.newArrayList();
-      getTargets(e, targetExpressions);
-
-      BlazeSyncParams syncParams =
-          new BlazeSyncParams.Builder("Partial Sync", BlazeSyncParams.SyncMode.PARTIAL)
-              .addTargetExpressions(targetExpressions)
-              .build();
-
-      BlazeSyncManager.getInstance(project).requestProjectSync(syncParams);
-    }
+  protected void actionPerformedInBlazeProject(Project project, AnActionEvent e) {
+    VirtualFile virtualFile = getSelectedFile(e);
+    List<TargetExpression> targets = getTargets(project, virtualFile);
+    BlazeSyncManager.getInstance(project).partialSync(targets);
   }
 
   @Override
-  protected void doUpdate(AnActionEvent e) {
-    super.doUpdate(e);
-    List<TargetExpression> targets = Lists.newArrayList();
-    String objectName = getTargets(e, targets);
-
-    boolean enabled = objectName != null && !targets.isEmpty();
-    Presentation presentation = e.getPresentation();
-    presentation.setEnabled(enabled);
-
-    if (enabled) {
-      presentation.setText(
-          String.format("Partially Sync %s with %s", objectName, buildSystemName(e.getProject())));
-    } else {
-      presentation.setText(String.format("Partial %s Sync", buildSystemName(e.getProject())));
-    }
+  protected void updateForBlazeProject(Project project, AnActionEvent e) {
+    VirtualFile virtualFile = getSelectedFile(e);
+    List<TargetExpression> targets = getTargets(project, virtualFile);
+    ActionPresentationHelper.of(e)
+        .disableIf(BlazeSyncStatus.getInstance(project).syncInProgress())
+        .disableIf(targets.isEmpty())
+        .setTextWithSubject("Partially Sync File", "Partially Sync %s", virtualFile)
+        .disableWithoutSubject()
+        .commit();
   }
 
-  private static String buildSystemName(@Nullable Project project) {
-    return Blaze.buildSystemName(project);
-  }
-
-  @Nullable
-  private String getTargets(AnActionEvent e, List<TargetExpression> targets) {
-    Project project = e.getProject();
+  private VirtualFile getSelectedFile(AnActionEvent e) {
     VirtualFile virtualFile = e.getData(CommonDataKeys.VIRTUAL_FILE);
-    if (project == null || virtualFile == null || !virtualFile.isInLocalFileSystem()) {
+    if (virtualFile == null || !virtualFile.isInLocalFileSystem()) {
       return null;
     }
+    return virtualFile;
+  }
 
+  private static List<TargetExpression> getTargets(
+      Project project, @Nullable VirtualFile virtualFile) {
+    if (virtualFile == null) {
+      return ImmutableList.of();
+    }
+    List<TargetExpression> targets = Lists.newArrayList();
     WorkspaceRoot workspaceRoot = WorkspaceRoot.fromProject(project);
     SourceToTargetMap.getInstance(project);
-
-    String objectName = virtualFile.isDirectory() ? "Package" : "File";
     if (!virtualFile.isDirectory()) {
       targets.addAll(
           SourceToTargetMap.getInstance(project)
@@ -109,7 +96,6 @@
         }
       }
     }
-
-    return objectName;
+    return targets;
   }
 }
diff --git a/base/src/com/google/idea/blaze/base/sync/actions/ShowPerformanceWarningsToggleAction.java b/base/src/com/google/idea/blaze/base/sync/actions/ShowPerformanceWarningsToggleAction.java
index d7c6e96..9cb2b46 100644
--- a/base/src/com/google/idea/blaze/base/sync/actions/ShowPerformanceWarningsToggleAction.java
+++ b/base/src/com/google/idea/blaze/base/sync/actions/ShowPerformanceWarningsToggleAction.java
@@ -15,12 +15,12 @@
  */
 package com.google.idea.blaze.base.sync.actions;
 
-import com.google.idea.blaze.base.actions.BlazeToggleAction;
+import com.google.idea.blaze.base.actions.BlazeProjectToggleAction;
 import com.google.idea.blaze.base.settings.BlazeUserSettings;
 import com.intellij.openapi.actionSystem.AnActionEvent;
 
 /** Manages a tick box of whether to show performance warnings. */
-public class ShowPerformanceWarningsToggleAction extends BlazeToggleAction {
+public class ShowPerformanceWarningsToggleAction extends BlazeProjectToggleAction {
   @Override
   public boolean isSelected(AnActionEvent e) {
     return BlazeUserSettings.getInstance().getShowPerformanceWarnings();
diff --git a/base/src/com/google/idea/blaze/base/sync/actions/SyncWorkingSetAction.java b/base/src/com/google/idea/blaze/base/sync/actions/SyncWorkingSetAction.java
index 950f473..f72294b 100644
--- a/base/src/com/google/idea/blaze/base/sync/actions/SyncWorkingSetAction.java
+++ b/base/src/com/google/idea/blaze/base/sync/actions/SyncWorkingSetAction.java
@@ -15,25 +15,29 @@
  */
 package com.google.idea.blaze.base.sync.actions;
 
-import com.google.idea.blaze.base.actions.BlazeAction;
+import com.google.idea.blaze.base.actions.BlazeProjectAction;
 import com.google.idea.blaze.base.sync.BlazeSyncManager;
-import com.google.idea.blaze.base.sync.BlazeSyncParams;
-import com.google.idea.blaze.base.sync.BlazeSyncParams.SyncMode;
+import com.google.idea.blaze.base.sync.status.BlazeSyncStatus;
 import com.intellij.openapi.actionSystem.AnActionEvent;
+import com.intellij.openapi.actionSystem.Presentation;
 import com.intellij.openapi.project.Project;
 
-/** Allows a partial sync of the project depending on what's been selected. */
-public class SyncWorkingSetAction extends BlazeAction {
-  @Override
-  public void actionPerformed(AnActionEvent e) {
-    Project project = e.getProject();
-    if (project != null) {
-      BlazeSyncParams syncParams =
-          new BlazeSyncParams.Builder("Sync Working Set", SyncMode.PARTIAL)
-              .addWorkingSet(true)
-              .build();
+/** Allows a partial sync of the working set. */
+public class SyncWorkingSetAction extends BlazeProjectAction {
 
-      BlazeSyncManager.getInstance(project).requestProjectSync(syncParams);
-    }
+  @Override
+  protected void actionPerformedInBlazeProject(Project project, AnActionEvent e) {
+    BlazeSyncManager.getInstance(project).workingSetSync();
+    updateStatus(project, e);
+  }
+
+  @Override
+  protected void updateForBlazeProject(Project project, AnActionEvent e) {
+    updateStatus(project, e);
+  }
+
+  private static void updateStatus(Project project, AnActionEvent e) {
+    Presentation presentation = e.getPresentation();
+    presentation.setEnabled(!BlazeSyncStatus.getInstance(project).syncInProgress());
   }
 }
diff --git a/base/src/com/google/idea/blaze/base/sync/aspects/BlazeIdeInterface.java b/base/src/com/google/idea/blaze/base/sync/aspects/BlazeIdeInterface.java
index 4051083..c355d5d 100644
--- a/base/src/com/google/idea/blaze/base/sync/aspects/BlazeIdeInterface.java
+++ b/base/src/com/google/idea/blaze/base/sync/aspects/BlazeIdeInterface.java
@@ -16,6 +16,7 @@
 package com.google.idea.blaze.base.sync.aspects;
 
 import com.google.idea.blaze.base.ideinfo.TargetMap;
+import com.google.idea.blaze.base.model.BlazeVersionData;
 import com.google.idea.blaze.base.model.SyncState;
 import com.google.idea.blaze.base.model.primitives.TargetExpression;
 import com.google.idea.blaze.base.model.primitives.WorkspaceRoot;
@@ -73,6 +74,7 @@
       BlazeContext context,
       WorkspaceRoot workspaceRoot,
       ProjectViewSet projectViewSet,
+      BlazeVersionData blazeVersionData,
       List<TargetExpression> targets,
       WorkspaceLanguageSettings workspaceLanguageSettings,
       ArtifactLocationDecoder artifactLocationDecoder,
@@ -90,5 +92,19 @@
       BlazeContext context,
       WorkspaceRoot workspaceRoot,
       ProjectViewSet projectViewSet,
+      BlazeVersionData blazeVersionData,
+      List<TargetExpression> targets);
+
+  /**
+   * Attempts to compile the requested ide artifacts.
+   *
+   * <p>Amounts to a build of the ide-compile output group.
+   */
+  BuildResult compileIdeArtifacts(
+      Project project,
+      BlazeContext context,
+      WorkspaceRoot workspaceRoot,
+      ProjectViewSet projectViewSet,
+      BlazeVersionData blazeVersionData,
       List<TargetExpression> targets);
 }
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 f44f200..36e29af 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
@@ -36,6 +36,7 @@
 import com.google.idea.blaze.base.ideinfo.TargetMap;
 import com.google.idea.blaze.base.issueparser.IssueOutputLineProcessor;
 import com.google.idea.blaze.base.metrics.Action;
+import com.google.idea.blaze.base.model.BlazeVersionData;
 import com.google.idea.blaze.base.model.SyncState;
 import com.google.idea.blaze.base.model.primitives.TargetExpression;
 import com.google.idea.blaze.base.model.primitives.WorkspaceRoot;
@@ -91,6 +92,7 @@
       BlazeContext context,
       WorkspaceRoot workspaceRoot,
       ProjectViewSet projectViewSet,
+      BlazeVersionData blazeVersionData,
       List<TargetExpression> targets,
       WorkspaceLanguageSettings workspaceLanguageSettings,
       ArtifactLocationDecoder artifactLocationDecoder,
@@ -106,7 +108,7 @@
     }
 
     // If the aspect strategy has changed, redo everything from scratch
-    final AspectStrategy aspectStrategy = getAspectStrategy(project);
+    final AspectStrategy aspectStrategy = getAspectStrategy(project, blazeVersionData);
     if (prevState != null
         && !Objects.equal(prevState.aspectStrategyName, aspectStrategy.getName())) {
       prevState = null;
@@ -375,8 +377,38 @@
       BlazeContext context,
       WorkspaceRoot workspaceRoot,
       ProjectViewSet projectViewSet,
+      BlazeVersionData blazeVersionData,
       List<TargetExpression> targets) {
-    AspectStrategy aspectStrategy = getAspectStrategy(project);
+    return resolveIdeArtifacts(
+        project, context, workspaceRoot, projectViewSet, blazeVersionData, targets, false);
+  }
+
+  @Override
+  public BuildResult compileIdeArtifacts(
+      Project project,
+      BlazeContext context,
+      WorkspaceRoot workspaceRoot,
+      ProjectViewSet projectViewSet,
+      BlazeVersionData blazeVersionData,
+      List<TargetExpression> targets) {
+    boolean ideCompile = hasIdeCompileOutputGroup(blazeVersionData);
+    return resolveIdeArtifacts(
+        project, context, workspaceRoot, projectViewSet, blazeVersionData, targets, ideCompile);
+  }
+
+  private static boolean hasIdeCompileOutputGroup(BlazeVersionData blazeVersionData) {
+    return blazeVersionData.bazelIsAtLeastVersion(0, 4, 3);
+  }
+
+  private static BuildResult resolveIdeArtifacts(
+      Project project,
+      BlazeContext context,
+      WorkspaceRoot workspaceRoot,
+      ProjectViewSet projectViewSet,
+      BlazeVersionData blazeVersionData,
+      List<TargetExpression> targets,
+      boolean useIdeCompileOutputGroup) {
+    AspectStrategy aspectStrategy = getAspectStrategy(project, blazeVersionData);
 
     BlazeCommand.Builder blazeCommandBuilder =
         BlazeCommand.builder(Blaze.getBuildSystem(project), BlazeCommandName.BUILD)
@@ -385,7 +417,11 @@
             .addBlazeFlags(BlazeFlags.KEEP_GOING)
             .addBlazeFlags(BlazeFlags.buildFlags(project, projectViewSet));
 
-    aspectStrategy.modifyIdeResolveCommand(blazeCommandBuilder);
+    if (useIdeCompileOutputGroup) {
+      aspectStrategy.modifyIdeCompileCommand(blazeCommandBuilder);
+    } else {
+      aspectStrategy.modifyIdeResolveCommand(blazeCommandBuilder);
+    }
 
     BlazeCommand blazeCommand = blazeCommandBuilder.build();
 
@@ -402,9 +438,10 @@
     return BuildResult.fromExitCode(retVal);
   }
 
-  private static AspectStrategy getAspectStrategy(Project project) {
+  private static AspectStrategy getAspectStrategy(
+      Project project, BlazeVersionData blazeVersionData) {
     for (AspectStrategyProvider provider : AspectStrategyProvider.EP_NAME.getExtensions()) {
-      AspectStrategy aspectStrategy = provider.getAspectStrategy(project);
+      AspectStrategy aspectStrategy = provider.getAspectStrategy(project, blazeVersionData);
       if (aspectStrategy != null) {
         return aspectStrategy;
       }
diff --git a/base/src/com/google/idea/blaze/base/sync/aspects/IdeInfoFromProtobuf.java b/base/src/com/google/idea/blaze/base/sync/aspects/IdeInfoFromProtobuf.java
index f3f0557..c140587 100644
--- a/base/src/com/google/idea/blaze/base/sync/aspects/IdeInfoFromProtobuf.java
+++ b/base/src/com/google/idea/blaze/base/sync/aspects/IdeInfoFromProtobuf.java
@@ -150,8 +150,7 @@
   }
 
   private static TargetKey makeTargetKey(IntellijIdeInfo.TargetKey key) {
-    return TargetKey.forGeneralTarget(
-        new Label(key.getLabel()), Strings.emptyToNull(key.getAspectId()));
+    return TargetKey.forGeneralTarget(new Label(key.getLabel()), key.getAspectIdsList());
   }
 
   private static Dependency makeDependency(IntellijIdeInfo.Dependency dep) {
diff --git a/base/src/com/google/idea/blaze/base/sync/aspects/strategy/AspectStrategy.java b/base/src/com/google/idea/blaze/base/sync/aspects/strategy/AspectStrategy.java
index 598d8b6..d63630e 100644
--- a/base/src/com/google/idea/blaze/base/sync/aspects/strategy/AspectStrategy.java
+++ b/base/src/com/google/idea/blaze/base/sync/aspects/strategy/AspectStrategy.java
@@ -28,6 +28,8 @@
 
   void modifyIdeResolveCommand(BlazeCommand.Builder blazeCommandBuilder);
 
+  void modifyIdeCompileCommand(BlazeCommand.Builder blazeCommandBuilder);
+
   String getAspectOutputFileExtension();
 
   IntellijIdeInfo.TargetIdeInfo readAspectFile(InputStream inputStream) throws IOException;
diff --git a/base/src/com/google/idea/blaze/base/sync/aspects/strategy/AspectStrategyNative.java b/base/src/com/google/idea/blaze/base/sync/aspects/strategy/AspectStrategyNative.java
index df88c4f..63787db 100644
--- a/base/src/com/google/idea/blaze/base/sync/aspects/strategy/AspectStrategyNative.java
+++ b/base/src/com/google/idea/blaze/base/sync/aspects/strategy/AspectStrategyNative.java
@@ -22,6 +22,7 @@
 
 /** Aspect strategy for native. */
 public class AspectStrategyNative implements AspectStrategy {
+
   @Override
   public String getName() {
     return "NativeAspect";
@@ -42,6 +43,13 @@
   }
 
   @Override
+  public void modifyIdeCompileCommand(BlazeCommand.Builder blazeCommandBuilder) {
+    blazeCommandBuilder
+        .addBlazeFlags("--aspects=AndroidStudioInfoAspect")
+        .addBlazeFlags("--output_groups=ide-compile");
+  }
+
+  @Override
   public String getAspectOutputFileExtension() {
     return ".aswb-build";
   }
diff --git a/base/src/com/google/idea/blaze/base/sync/aspects/strategy/AspectStrategyProvider.java b/base/src/com/google/idea/blaze/base/sync/aspects/strategy/AspectStrategyProvider.java
index 0fdd2b5..13c7586 100644
--- a/base/src/com/google/idea/blaze/base/sync/aspects/strategy/AspectStrategyProvider.java
+++ b/base/src/com/google/idea/blaze/base/sync/aspects/strategy/AspectStrategyProvider.java
@@ -15,6 +15,7 @@
  */
 package com.google.idea.blaze.base.sync.aspects.strategy;
 
+import com.google.idea.blaze.base.model.BlazeVersionData;
 import com.intellij.openapi.extensions.ExtensionPointName;
 import com.intellij.openapi.project.Project;
 import javax.annotation.Nullable;
@@ -25,5 +26,5 @@
       ExtensionPointName.create("com.google.idea.blaze.AspectStrategyProvider");
 
   @Nullable
-  AspectStrategy getAspectStrategy(Project project);
+  AspectStrategy getAspectStrategy(Project project, BlazeVersionData blazeVersionData);
 }
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 9042523..f3b1029 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
@@ -15,14 +15,19 @@
  */
 package com.google.idea.blaze.base.sync.aspects.strategy;
 
+import com.google.idea.blaze.base.model.BlazeVersionData;
 import com.google.idea.common.experiments.BoolExperiment;
 import com.intellij.openapi.project.Project;
 
 class AspectStrategyProviderBazel implements AspectStrategyProvider {
-  BoolExperiment useSkylarkAspect = new BoolExperiment("use.skylark.aspect.bazel", false);
+  private static final BoolExperiment useSkylarkAspect =
+      new BoolExperiment("use.skylark.aspect.bazel", true);
 
   @Override
-  public AspectStrategy getAspectStrategy(Project project) {
-    return useSkylarkAspect.getValue() ? new AspectStrategySkylark() : new AspectStrategyNative();
+  public AspectStrategy getAspectStrategy(Project project, BlazeVersionData blazeVersionData) {
+    boolean canUseSkylark =
+        useSkylarkAspect.getValue() && blazeVersionData.bazelIsAtLeastVersion(0, 4, 3);
+
+    return canUseSkylark ? new AspectStrategySkylark() : new AspectStrategyNative();
   }
 }
diff --git a/base/src/com/google/idea/blaze/base/sync/aspects/strategy/AspectStrategySkylark.java b/base/src/com/google/idea/blaze/base/sync/aspects/strategy/AspectStrategySkylark.java
index 0139f87..aa3eadc 100644
--- a/base/src/com/google/idea/blaze/base/sync/aspects/strategy/AspectStrategySkylark.java
+++ b/base/src/com/google/idea/blaze/base/sync/aspects/strategy/AspectStrategySkylark.java
@@ -18,6 +18,7 @@
 import static java.nio.charset.StandardCharsets.UTF_8;
 
 import com.google.idea.blaze.base.command.BlazeCommand;
+import com.google.idea.blaze.base.command.BlazeCommand.Builder;
 import com.google.repackaged.devtools.intellij.ideinfo.IntellijIdeInfo;
 import com.google.repackaged.protobuf.TextFormat;
 import java.io.IOException;
@@ -33,7 +34,7 @@
   }
 
   protected String getAspectFlag() {
-    return "--aspects=@bazel_tools://tools/ide/intellij_info.bzl%intellij_info_aspect";
+    return "--aspects=@bazel_tools//tools/ide:intellij_info.bzl%intellij_info_aspect";
   }
 
   @Override
@@ -51,6 +52,13 @@
   }
 
   @Override
+  public void modifyIdeCompileCommand(Builder blazeCommandBuilder) {
+    blazeCommandBuilder
+        .addBlazeFlags(getAspectFlag())
+        .addBlazeFlags("--output_groups=intellij-compile");
+  }
+
+  @Override
   public String getAspectOutputFileExtension() {
     return ".intellij-info.txt";
   }
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 871274e..d8e72f6 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
@@ -24,6 +24,7 @@
 import com.google.idea.blaze.base.model.primitives.WorkspaceRoot;
 import com.google.idea.blaze.base.projectview.ProjectViewSet;
 import com.google.idea.blaze.base.scope.BlazeContext;
+import com.google.idea.blaze.base.scope.output.IssueOutput;
 import com.google.idea.blaze.base.settings.Blaze;
 import com.google.idea.blaze.base.sync.SourceFolderProvider;
 import com.google.idea.blaze.base.sync.projectview.ImportRoots;
@@ -82,6 +83,15 @@
       ImmutableMap<VirtualFile, SourceFolder> sourceFolders =
           provider.initializeSourceFolders(contentEntry);
       VirtualFile rootFile = getVirtualFile(root);
+      if (rootFile == null) {
+        IssueOutput.warn(
+                String.format(
+                    "Could not find directory %s. Your 'test_sources' project view "
+                        + "attribute will not have any effect. Please resync.",
+                    workspaceRoot))
+            .submit(context);
+        continue;
+      }
       SourceFolder rootSource = sourceFolders.get(rootFile);
       walkFileSystem(
           workspaceRoot,
@@ -121,11 +131,11 @@
     SourceFolder current = sourceFolders.get(file);
     SourceFolder currentOrParent = current != null ? current : parent;
     if (isTest != currentOrParent.isTestSource()) {
+      currentOrParent =
+          provider.setSourceFolderForLocation(contentEntry, currentOrParent, file, isTest);
       if (current != null) {
         contentEntry.removeSourceFolder(current);
       }
-      currentOrParent =
-          provider.setSourceFolderForLocation(contentEntry, currentOrParent, file, isTest);
     }
     for (VirtualFile child : file.getChildren()) {
       walkFileSystem(
diff --git a/base/src/com/google/idea/blaze/base/sync/status/BlazeSyncStatus.java b/base/src/com/google/idea/blaze/base/sync/status/BlazeSyncStatus.java
index aa98c7e..a1e3f7c 100644
--- a/base/src/com/google/idea/blaze/base/sync/status/BlazeSyncStatus.java
+++ b/base/src/com/google/idea/blaze/base/sync/status/BlazeSyncStatus.java
@@ -30,10 +30,13 @@
 
   SyncStatus getStatus();
 
+
   static BlazeSyncStatus getInstance(Project project) {
     return ServiceManager.getService(project, BlazeSyncStatus.class);
   }
 
+  boolean syncInProgress();
+
   void setDirty();
 
   void queueAutomaticSyncIfDirty();
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 396f27d..ee60cd7 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
@@ -23,7 +23,6 @@
 import com.google.idea.blaze.base.sync.BlazeSyncParams.SyncMode;
 import com.google.idea.blaze.base.sync.SyncListener.SyncResult;
 import com.intellij.openapi.diagnostic.Logger;
-import com.intellij.openapi.editor.Document;
 import com.intellij.openapi.fileEditor.FileDocumentManager;
 import com.intellij.openapi.fileEditor.FileEditorManager;
 import com.intellij.openapi.fileEditor.FileEditorManagerAdapter;
@@ -54,7 +53,7 @@
 
   private final Project project;
 
-  public final AtomicBoolean syncInProgress = new AtomicBoolean(false);
+  private final AtomicBoolean syncInProgress = new AtomicBoolean(false);
   private final AtomicBoolean syncPending = new AtomicBoolean(false);
 
   /** has a BUILD file changed since the last sync started */
@@ -86,6 +85,11 @@
     return dirty ? SyncStatus.DIRTY : SyncStatus.CLEAN;
   }
 
+  @Override
+  public boolean syncInProgress() {
+    return syncInProgress.get();
+  }
+
   public void syncStarted() {
     syncPending.set(false);
     syncInProgress.set(true);
@@ -189,10 +193,9 @@
 
     private void processEvent(@Nullable VirtualFile file) {
       if (isSyncSensitiveFile(file)) {
-        FileDocumentManager manager = FileDocumentManager.getInstance();
-        Document doc = manager.getCachedDocument(file);
-        if (doc != null) {
-          manager.saveDocument(doc);
+        FileDocumentManager fileDocumentManager = FileDocumentManager.getInstance();
+        if (fileDocumentManager.isFileModified(file)) {
+          setDirty();
         }
       }
     }
diff --git a/base/src/com/google/idea/blaze/base/targetmaps/TransitiveDependencyMap.java b/base/src/com/google/idea/blaze/base/targetmaps/TransitiveDependencyMap.java
new file mode 100644
index 0000000..f973c8d
--- /dev/null
+++ b/base/src/com/google/idea/blaze/base/targetmaps/TransitiveDependencyMap.java
@@ -0,0 +1,79 @@
+/*
+ * 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.targetmaps;
+
+import com.google.common.collect.ImmutableCollection;
+import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.Queues;
+import com.google.common.collect.Sets;
+import com.google.idea.blaze.base.ideinfo.TargetIdeInfo;
+import com.google.idea.blaze.base.ideinfo.TargetKey;
+import com.google.idea.blaze.base.ideinfo.TargetMap;
+import com.google.idea.blaze.base.model.BlazeProjectData;
+import com.google.idea.blaze.base.sync.data.BlazeProjectDataManager;
+import com.intellij.openapi.components.ServiceManager;
+import com.intellij.openapi.project.Project;
+import java.util.List;
+import java.util.Queue;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+/** Handy class to find all transitive dependencies of a given target */
+public class TransitiveDependencyMap {
+  private final Project project;
+
+  public static TransitiveDependencyMap getInstance(Project project) {
+    return ServiceManager.getService(project, TransitiveDependencyMap.class);
+  }
+
+  public TransitiveDependencyMap(Project project) {
+    this.project = project;
+  }
+
+  public ImmutableCollection<TargetKey> getTransitiveDependencies(TargetKey targetKey) {
+    BlazeProjectData blazeProjectData =
+        BlazeProjectDataManager.getInstance(project).getBlazeProjectData();
+    if (blazeProjectData == null) {
+      return ImmutableSet.of();
+    }
+    // TODO: see if we need caching.
+    return getTransitiveDependencies(targetKey, blazeProjectData.targetMap);
+  }
+
+  private static ImmutableCollection<TargetKey> getTransitiveDependencies(
+      TargetKey targetKey, TargetMap targetMap) {
+    Queue<TargetKey> targetsToVisit = Queues.newArrayDeque();
+    Set<TargetKey> transitiveDependencies = Sets.newHashSet();
+    targetsToVisit.add(targetKey);
+    while (!targetsToVisit.isEmpty()) {
+      TargetIdeInfo currentTarget = targetMap.get(targetsToVisit.remove());
+      if (currentTarget == null) {
+        continue;
+      }
+      List<TargetKey> newDependencies =
+          currentTarget
+              .dependencies
+              .stream()
+              .map(d -> TargetKey.forPlainTarget(d.targetKey.label))
+              // Get rid of the ones we've already seen.
+              .filter(r -> !transitiveDependencies.contains(r))
+              .collect(Collectors.toList());
+      targetsToVisit.addAll(newDependencies);
+      transitiveDependencies.addAll(newDependencies);
+    }
+    return ImmutableSet.copyOf(transitiveDependencies);
+  }
+}
diff --git a/base/src/com/google/idea/blaze/base/util/SaveUtil.java b/base/src/com/google/idea/blaze/base/util/SaveUtil.java
index 6454a7a..949f201 100644
--- a/base/src/com/google/idea/blaze/base/util/SaveUtil.java
+++ b/base/src/com/google/idea/blaze/base/util/SaveUtil.java
@@ -15,18 +15,13 @@
  */
 package com.google.idea.blaze.base.util;
 
+import com.google.idea.sdkcompat.transactions.Transactions;
 import com.intellij.openapi.fileEditor.FileDocumentManager;
-import com.intellij.util.ui.UIUtil;
 
 /** Utility for saving all files. */
 public class SaveUtil {
   public static void saveAllFiles() {
-    UIUtil.invokeAndWaitIfNeeded(
-        new Runnable() {
-          @Override
-          public void run() {
-            FileDocumentManager.getInstance().saveAllDocuments();
-          }
-        });
+    Transactions.submitTransactionAndWait(
+        () -> FileDocumentManager.getInstance().saveAllDocuments());
   }
 }
diff --git a/base/src/com/google/idea/blaze/base/vcs/BlazeVcsHandler.java b/base/src/com/google/idea/blaze/base/vcs/BlazeVcsHandler.java
index 16e6909..b05e69a 100644
--- a/base/src/com/google/idea/blaze/base/vcs/BlazeVcsHandler.java
+++ b/base/src/com/google/idea/blaze/base/vcs/BlazeVcsHandler.java
@@ -20,6 +20,7 @@
 import com.google.idea.blaze.base.model.primitives.WorkspaceRoot;
 import com.google.idea.blaze.base.projectview.ProjectViewSet;
 import com.google.idea.blaze.base.scope.BlazeContext;
+import com.google.idea.blaze.base.settings.Blaze;
 import com.google.idea.blaze.base.settings.Blaze.BuildSystem;
 import com.google.idea.blaze.base.sync.workspace.BlazeRoots;
 import com.google.idea.blaze.base.sync.workspace.WorkingSet;
@@ -33,6 +34,18 @@
   ExtensionPointName<BlazeVcsHandler> EP_NAME =
       ExtensionPointName.create("com.google.idea.blaze.VcsHandler");
 
+  @Nullable
+  static BlazeVcsHandler vcsHandlerForProject(Project project) {
+    BuildSystem buildSystem = Blaze.getBuildSystem(project);
+    WorkspaceRoot workspaceRoot = WorkspaceRoot.fromProject(project);
+    for (BlazeVcsHandler candidate : BlazeVcsHandler.EP_NAME.getExtensions()) {
+      if (candidate.handlesProject(buildSystem, workspaceRoot)) {
+        return candidate;
+      }
+    }
+    return null;
+  }
+
   /** Returns the name of this VCS, eg. "git" or "hg" */
   String getVcsName();
 
diff --git a/base/src/com/google/idea/blaze/base/wizard2/BlazeWizardOptionProvider.java b/base/src/com/google/idea/blaze/base/wizard2/BlazeWizardOptionProvider.java
index 20a7e4d..1739755 100644
--- a/base/src/com/google/idea/blaze/base/wizard2/BlazeWizardOptionProvider.java
+++ b/base/src/com/google/idea/blaze/base/wizard2/BlazeWizardOptionProvider.java
@@ -15,16 +15,17 @@
  */
 package com.google.idea.blaze.base.wizard2;
 
-import com.intellij.openapi.extensions.ExtensionPointName;
+import com.intellij.openapi.components.ServiceManager;
 import java.util.Collection;
 
 /** Provides options during the import process. */
 public interface BlazeWizardOptionProvider {
-  ExtensionPointName<BlazeWizardOptionProvider> EP_NAME =
-      ExtensionPointName.create("com.google.idea.blaze.BlazeWizardOptionProvider");
-
   Collection<BlazeSelectWorkspaceOption> getSelectWorkspaceOptions(BlazeNewProjectBuilder builder);
 
   Collection<BlazeSelectProjectViewOption> getSelectProjectViewOptions(
       BlazeNewProjectBuilder builder);
+
+  static BlazeWizardOptionProvider getInstance() {
+    return ServiceManager.getService(BlazeWizardOptionProvider.class);
+  }
 }
diff --git a/base/src/com/google/idea/blaze/base/wizard2/CopyExternalProjectViewOption.java b/base/src/com/google/idea/blaze/base/wizard2/CopyExternalProjectViewOption.java
index 62db2c5..ae00949 100644
--- a/base/src/com/google/idea/blaze/base/wizard2/CopyExternalProjectViewOption.java
+++ b/base/src/com/google/idea/blaze/base/wizard2/CopyExternalProjectViewOption.java
@@ -35,14 +35,15 @@
 import javax.swing.JComponent;
 import javax.swing.JLabel;
 
-class CopyExternalProjectViewOption implements BlazeSelectProjectViewOption {
+/** Copies an external project view from anywhere on the user's file system */
+public class CopyExternalProjectViewOption implements BlazeSelectProjectViewOption {
   private static final String LAST_WORKSPACE_PATH = "copy-external.last-project-view-path";
 
   final BlazeWizardUserSettings userSettings;
   final JComponent component;
   final TextFieldWithStoredHistory projectViewPathField;
 
-  CopyExternalProjectViewOption(BlazeNewProjectBuilder builder) {
+  public CopyExternalProjectViewOption(BlazeNewProjectBuilder builder) {
     this.userSettings = builder.getUserSettings();
 
     this.projectViewPathField = new TextFieldWithStoredHistory(LAST_WORKSPACE_PATH);
diff --git a/base/src/com/google/idea/blaze/base/wizard2/CreateFromScratchProjectViewOption.java b/base/src/com/google/idea/blaze/base/wizard2/CreateFromScratchProjectViewOption.java
index 366e424..3e8ebf7 100644
--- a/base/src/com/google/idea/blaze/base/wizard2/CreateFromScratchProjectViewOption.java
+++ b/base/src/com/google/idea/blaze/base/wizard2/CreateFromScratchProjectViewOption.java
@@ -19,7 +19,8 @@
 import javax.annotation.Nullable;
 import javax.swing.JComponent;
 
-class CreateFromScratchProjectViewOption implements BlazeSelectProjectViewOption {
+/** Creates an empty project view */
+public class CreateFromScratchProjectViewOption implements BlazeSelectProjectViewOption {
   @Override
   public String getOptionName() {
     return "create-from-scratch";
diff --git a/base/src/com/google/idea/blaze/base/wizard2/GenerateFromBuildFileSelectProjectViewOption.java b/base/src/com/google/idea/blaze/base/wizard2/GenerateFromBuildFileSelectProjectViewOption.java
index e2e41d9..b2edeff 100644
--- a/base/src/com/google/idea/blaze/base/wizard2/GenerateFromBuildFileSelectProjectViewOption.java
+++ b/base/src/com/google/idea/blaze/base/wizard2/GenerateFromBuildFileSelectProjectViewOption.java
@@ -45,7 +45,8 @@
 import javax.swing.JComponent;
 import javax.swing.JLabel;
 
-class GenerateFromBuildFileSelectProjectViewOption implements BlazeSelectProjectViewOption {
+/** Generates a project view given a BUILD file */
+public class GenerateFromBuildFileSelectProjectViewOption implements BlazeSelectProjectViewOption {
   private static final String LAST_WORKSPACE_PATH = "generate-from-build-file.last-workspace-path";
   private final BlazeNewProjectBuilder builder;
   private final BlazeWizardUserSettings userSettings;
diff --git a/base/src/com/google/idea/blaze/base/wizard2/ImportFromWorkspaceProjectViewOption.java b/base/src/com/google/idea/blaze/base/wizard2/ImportFromWorkspaceProjectViewOption.java
index c2d47e1..bdec2eb 100644
--- a/base/src/com/google/idea/blaze/base/wizard2/ImportFromWorkspaceProjectViewOption.java
+++ b/base/src/com/google/idea/blaze/base/wizard2/ImportFromWorkspaceProjectViewOption.java
@@ -39,7 +39,8 @@
 import javax.swing.JComponent;
 import javax.swing.JLabel;
 
-class ImportFromWorkspaceProjectViewOption implements BlazeSelectProjectViewOption {
+/** Imports a project view from source control */
+public class ImportFromWorkspaceProjectViewOption implements BlazeSelectProjectViewOption {
   private static final String LAST_WORKSPACE_PATH = "import-from-workspace.last-workspace-path";
 
   final BlazeNewProjectBuilder builder;
@@ -47,7 +48,7 @@
   final JComponent component;
   final TextFieldWithStoredHistory projectViewPathField;
 
-  ImportFromWorkspaceProjectViewOption(BlazeNewProjectBuilder builder) {
+  public ImportFromWorkspaceProjectViewOption(BlazeNewProjectBuilder builder) {
     this.builder = builder;
     this.userSettings = builder.getUserSettings();
 
diff --git a/base/src/com/google/idea/blaze/base/wizard2/UseExistingBazelWorkspaceOption.java b/base/src/com/google/idea/blaze/base/wizard2/UseExistingBazelWorkspaceOption.java
index 2b8c058..51af079 100644
--- a/base/src/com/google/idea/blaze/base/wizard2/UseExistingBazelWorkspaceOption.java
+++ b/base/src/com/google/idea/blaze/base/wizard2/UseExistingBazelWorkspaceOption.java
@@ -36,12 +36,13 @@
 import javax.swing.JComponent;
 import javax.swing.JLabel;
 
-class UseExistingBazelWorkspaceOption implements BlazeSelectWorkspaceOption {
+/** Allows importing an existing bazel workspace */
+public class UseExistingBazelWorkspaceOption implements BlazeSelectWorkspaceOption {
 
   private final JComponent component;
   private final TextFieldWithHistory directoryField;
 
-  UseExistingBazelWorkspaceOption(BlazeNewProjectBuilder builder) {
+  public UseExistingBazelWorkspaceOption(BlazeNewProjectBuilder builder) {
     this.directoryField = new TextFieldWithHistory();
     this.directoryField.setHistory(builder.getWorkspaceHistory(BuildSystem.Bazel));
     this.directoryField.setHistorySize(BlazeNewProjectBuilder.HISTORY_SIZE);
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 0307b5a..4f3be05 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
@@ -92,6 +92,7 @@
   private JTextField projectNameField;
   private HashCode paramsHash;
   private WorkspaceRoot workspaceRoot;
+  private WorkspacePathResolver workspacePathResolver;
 
   public BlazeEditProjectViewControl(BlazeNewProjectBuilder builder, Disposable parentDisposable) {
     this.projectViewUi = new ProjectViewUi(parentDisposable);
@@ -209,6 +210,7 @@
     }
 
     this.workspaceRoot = workspaceRoot;
+    this.workspacePathResolver = workspacePathResolver;
     projectNameField.setText(workspaceName);
     String defaultDataDir = getDefaultProjectDataDirectory(workspaceName);
     projectDataDirField.setText(defaultDataDir);
@@ -328,7 +330,8 @@
       return BlazeValidationResult.failure(projectViewParseError);
     }
 
-    ProjectViewValidator projectViewValidator = new ProjectViewValidator(projectViewSet);
+    ProjectViewValidator projectViewValidator =
+        new ProjectViewValidator(workspacePathResolver, projectViewSet);
     ProgressManager.getInstance()
         .runProcessWithProgressSynchronously(
             projectViewValidator, "Validating Project", false, null);
@@ -347,12 +350,15 @@
   }
 
   private static class ProjectViewValidator implements Runnable {
+    private final WorkspacePathResolver workspacePathResolver;
     private final ProjectViewSet projectViewSet;
 
     private boolean success;
     List<IssueOutput> errors = Lists.newArrayList();
 
-    ProjectViewValidator(ProjectViewSet projectViewSet) {
+    ProjectViewValidator(
+        WorkspacePathResolver workspacePathResolver, ProjectViewSet projectViewSet) {
+      this.workspacePathResolver = workspacePathResolver;
       this.projectViewSet = projectViewSet;
     }
 
@@ -379,8 +385,8 @@
       if (workspaceLanguageSettings == null) {
         return false;
       }
-      return ProjectViewVerifier.verifyProjectViewNoDisk(
-          context, projectViewSet, workspaceLanguageSettings);
+      return ProjectViewVerifier.verifyProjectView(
+          context, workspacePathResolver, projectViewSet, workspaceLanguageSettings);
     }
   }
 
diff --git a/base/src/com/google/idea/blaze/base/wizard2/ui/BlazeSelectProjectViewControl.java b/base/src/com/google/idea/blaze/base/wizard2/ui/BlazeSelectProjectViewControl.java
index a2b6467..4dc04cd 100644
--- a/base/src/com/google/idea/blaze/base/wizard2/ui/BlazeSelectProjectViewControl.java
+++ b/base/src/com/google/idea/blaze/base/wizard2/ui/BlazeSelectProjectViewControl.java
@@ -15,7 +15,6 @@
  */
 package com.google.idea.blaze.base.wizard2.ui;
 
-import com.google.common.collect.Lists;
 import com.google.idea.blaze.base.projectview.ProjectViewStorageManager;
 import com.google.idea.blaze.base.ui.BlazeValidationResult;
 import com.google.idea.blaze.base.wizard2.BlazeNewProjectBuilder;
@@ -30,11 +29,8 @@
   private BlazeSelectOptionControl<BlazeSelectProjectViewOption> selectOptionControl;
 
   public BlazeSelectProjectViewControl(BlazeNewProjectBuilder builder) {
-    Collection<BlazeSelectProjectViewOption> options = Lists.newArrayList();
-    for (BlazeWizardOptionProvider optionProvider :
-        BlazeWizardOptionProvider.EP_NAME.getExtensions()) {
-      options.addAll(optionProvider.getSelectProjectViewOptions(builder));
-    }
+    Collection<BlazeSelectProjectViewOption> options =
+        BlazeWizardOptionProvider.getInstance().getSelectProjectViewOptions(builder);
 
     this.selectOptionControl =
         new BlazeSelectOptionControl<BlazeSelectProjectViewOption>(builder, options) {
diff --git a/base/src/com/google/idea/blaze/base/wizard2/ui/BlazeSelectWorkspaceControl.java b/base/src/com/google/idea/blaze/base/wizard2/ui/BlazeSelectWorkspaceControl.java
index f0c6b45..e24b446 100644
--- a/base/src/com/google/idea/blaze/base/wizard2/ui/BlazeSelectWorkspaceControl.java
+++ b/base/src/com/google/idea/blaze/base/wizard2/ui/BlazeSelectWorkspaceControl.java
@@ -15,7 +15,6 @@
  */
 package com.google.idea.blaze.base.wizard2.ui;
 
-import com.google.common.collect.Lists;
 import com.google.idea.blaze.base.ui.BlazeValidationResult;
 import com.google.idea.blaze.base.wizard2.BlazeNewProjectBuilder;
 import com.google.idea.blaze.base.wizard2.BlazeSelectWorkspaceOption;
@@ -28,11 +27,8 @@
   BlazeSelectOptionControl<BlazeSelectWorkspaceOption> selectOptionControl;
 
   public BlazeSelectWorkspaceControl(BlazeNewProjectBuilder builder) {
-    Collection<BlazeSelectWorkspaceOption> options = Lists.newArrayList();
-    for (BlazeWizardOptionProvider optionProvider :
-        BlazeWizardOptionProvider.EP_NAME.getExtensions()) {
-      options.addAll(optionProvider.getSelectWorkspaceOptions(builder));
-    }
+    Collection<BlazeSelectWorkspaceOption> options =
+        BlazeWizardOptionProvider.getInstance().getSelectWorkspaceOptions(builder);
 
     this.selectOptionControl =
         new BlazeSelectOptionControl<BlazeSelectWorkspaceOption>(builder, options) {
diff --git a/base/tests/integrationtests/com/google/idea/blaze/base/lang/projectview/ProjectViewIntegrationTestCase.java b/base/tests/integrationtests/com/google/idea/blaze/base/lang/projectview/ProjectViewIntegrationTestCase.java
index 361b0bb..960660e 100644
--- a/base/tests/integrationtests/com/google/idea/blaze/base/lang/projectview/ProjectViewIntegrationTestCase.java
+++ b/base/tests/integrationtests/com/google/idea/blaze/base/lang/projectview/ProjectViewIntegrationTestCase.java
@@ -21,11 +21,12 @@
 import com.google.idea.blaze.base.EditorTestHelper;
 import com.google.idea.blaze.base.ideinfo.TargetMap;
 import com.google.idea.blaze.base.model.BlazeProjectData;
+import com.google.idea.blaze.base.model.BlazeVersionData;
+import com.google.idea.blaze.base.model.SyncState;
 import com.google.idea.blaze.base.model.primitives.ExecutionRootPath;
 import com.google.idea.blaze.base.sync.workspace.ArtifactLocationDecoder;
 import com.google.idea.blaze.base.sync.workspace.ArtifactLocationDecoderImpl;
 import com.google.idea.blaze.base.sync.workspace.BlazeRoots;
-import com.google.idea.blaze.base.sync.workspace.WorkingSet;
 import com.google.idea.blaze.base.sync.workspace.WorkspacePathResolver;
 import com.google.idea.blaze.base.sync.workspace.WorkspacePathResolverImpl;
 import org.junit.Before;
@@ -57,12 +58,11 @@
         new TargetMap(ImmutableMap.of()),
         ImmutableMap.of(),
         fakeRoots,
-        new WorkingSet(ImmutableList.of(), ImmutableList.of(), ImmutableList.of()),
+        new BlazeVersionData(),
         workspacePathResolver,
         artifactLocationDecoder,
         null,
-        null,
-        null,
+        new SyncState.Builder().build(),
         null);
   }
 }
diff --git a/base/tests/integrationtests/com/google/idea/blaze/base/run/BlazeCommandRunConfigurationGenericHandlerIntegrationTest.java b/base/tests/integrationtests/com/google/idea/blaze/base/run/BlazeCommandRunConfigurationGenericHandlerIntegrationTest.java
index c4f5242..eb8d69f 100644
--- a/base/tests/integrationtests/com/google/idea/blaze/base/run/BlazeCommandRunConfigurationGenericHandlerIntegrationTest.java
+++ b/base/tests/integrationtests/com/google/idea/blaze/base/run/BlazeCommandRunConfigurationGenericHandlerIntegrationTest.java
@@ -23,6 +23,7 @@
 import com.google.idea.blaze.base.command.BlazeCommandName;
 import com.google.idea.blaze.base.ideinfo.TargetMap;
 import com.google.idea.blaze.base.model.BlazeProjectData;
+import com.google.idea.blaze.base.model.BlazeVersionData;
 import com.google.idea.blaze.base.model.primitives.ExecutionRootPath;
 import com.google.idea.blaze.base.model.primitives.TargetExpression;
 import com.google.idea.blaze.base.run.BlazeCommandRunConfiguration.BlazeCommandRunConfigurationSettingsEditor;
@@ -31,7 +32,6 @@
 import com.google.idea.blaze.base.sync.workspace.ArtifactLocationDecoder;
 import com.google.idea.blaze.base.sync.workspace.ArtifactLocationDecoderImpl;
 import com.google.idea.blaze.base.sync.workspace.BlazeRoots;
-import com.google.idea.blaze.base.sync.workspace.WorkingSet;
 import com.google.idea.blaze.base.sync.workspace.WorkspacePathResolver;
 import com.google.idea.blaze.base.sync.workspace.WorkspacePathResolverImpl;
 import com.intellij.openapi.options.ConfigurationException;
@@ -79,12 +79,11 @@
         new TargetMap(ImmutableMap.of()),
         ImmutableMap.of(),
         fakeRoots,
-        new WorkingSet(ImmutableList.of(), ImmutableList.of(), ImmutableList.of()),
+        new BlazeVersionData(),
         workspacePathResolver,
         artifactLocationDecoder,
         null,
         null,
-        null,
         null);
   }
 
diff --git a/base/tests/integrationtests/com/google/idea/blaze/base/run/BlazeCommandRunConfigurationRunManagerImplTest.java b/base/tests/integrationtests/com/google/idea/blaze/base/run/BlazeCommandRunConfigurationRunManagerImplTest.java
index 6395e65..237fa7f 100644
--- a/base/tests/integrationtests/com/google/idea/blaze/base/run/BlazeCommandRunConfigurationRunManagerImplTest.java
+++ b/base/tests/integrationtests/com/google/idea/blaze/base/run/BlazeCommandRunConfigurationRunManagerImplTest.java
@@ -23,13 +23,13 @@
 import com.google.idea.blaze.base.BlazeIntegrationTestCase;
 import com.google.idea.blaze.base.ideinfo.TargetMap;
 import com.google.idea.blaze.base.model.BlazeProjectData;
+import com.google.idea.blaze.base.model.BlazeVersionData;
 import com.google.idea.blaze.base.model.primitives.ExecutionRootPath;
 import com.google.idea.blaze.base.model.primitives.Label;
 import com.google.idea.blaze.base.run.BlazeCommandRunConfiguration.BlazeCommandRunConfigurationSettingsEditor;
 import com.google.idea.blaze.base.sync.workspace.ArtifactLocationDecoder;
 import com.google.idea.blaze.base.sync.workspace.ArtifactLocationDecoderImpl;
 import com.google.idea.blaze.base.sync.workspace.BlazeRoots;
-import com.google.idea.blaze.base.sync.workspace.WorkingSet;
 import com.google.idea.blaze.base.sync.workspace.WorkspacePathResolver;
 import com.google.idea.blaze.base.sync.workspace.WorkspacePathResolverImpl;
 import com.intellij.execution.RunnerAndConfigurationSettings;
@@ -89,12 +89,11 @@
         new TargetMap(ImmutableMap.of()),
         ImmutableMap.of(),
         fakeRoots,
-        new WorkingSet(ImmutableList.of(), ImmutableList.of(), ImmutableList.of()),
+        new BlazeVersionData(),
         workspacePathResolver,
         artifactLocationDecoder,
         null,
         null,
-        null,
         null);
   }
 
diff --git a/base/tests/integrationtests/com/google/idea/blaze/base/run/BlazeCommandRunConfigurationSettingsEditorTest.java b/base/tests/integrationtests/com/google/idea/blaze/base/run/BlazeCommandRunConfigurationSettingsEditorTest.java
index 336930a..b9a2b48 100644
--- a/base/tests/integrationtests/com/google/idea/blaze/base/run/BlazeCommandRunConfigurationSettingsEditorTest.java
+++ b/base/tests/integrationtests/com/google/idea/blaze/base/run/BlazeCommandRunConfigurationSettingsEditorTest.java
@@ -22,13 +22,13 @@
 import com.google.idea.blaze.base.BlazeIntegrationTestCase;
 import com.google.idea.blaze.base.ideinfo.TargetMap;
 import com.google.idea.blaze.base.model.BlazeProjectData;
+import com.google.idea.blaze.base.model.BlazeVersionData;
 import com.google.idea.blaze.base.model.primitives.ExecutionRootPath;
 import com.google.idea.blaze.base.model.primitives.Label;
 import com.google.idea.blaze.base.run.BlazeCommandRunConfiguration.BlazeCommandRunConfigurationSettingsEditor;
 import com.google.idea.blaze.base.sync.workspace.ArtifactLocationDecoder;
 import com.google.idea.blaze.base.sync.workspace.ArtifactLocationDecoderImpl;
 import com.google.idea.blaze.base.sync.workspace.BlazeRoots;
-import com.google.idea.blaze.base.sync.workspace.WorkingSet;
 import com.google.idea.blaze.base.sync.workspace.WorkspacePathResolver;
 import com.google.idea.blaze.base.sync.workspace.WorkspacePathResolverImpl;
 import com.intellij.openapi.options.ConfigurationException;
@@ -70,12 +70,11 @@
         new TargetMap(ImmutableMap.of()),
         ImmutableMap.of(),
         fakeRoots,
-        new WorkingSet(ImmutableList.of(), ImmutableList.of(), ImmutableList.of()),
+        new BlazeVersionData(),
         workspacePathResolver,
         artifactLocationDecoder,
         null,
         null,
-        null,
         null);
   }
 
diff --git a/base/tests/integrationtests/com/google/idea/blaze/base/run/exporter/RunConfigurationSerializerTest.java b/base/tests/integrationtests/com/google/idea/blaze/base/run/exporter/RunConfigurationSerializerTest.java
new file mode 100644
index 0000000..fd5896b
--- /dev/null
+++ b/base/tests/integrationtests/com/google/idea/blaze/base/run/exporter/RunConfigurationSerializerTest.java
@@ -0,0 +1,157 @@
+/*
+ * 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.run.exporter;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+import com.google.idea.blaze.base.BlazeIntegrationTestCase;
+import com.google.idea.blaze.base.ideinfo.TargetMap;
+import com.google.idea.blaze.base.model.BlazeProjectData;
+import com.google.idea.blaze.base.model.BlazeVersionData;
+import com.google.idea.blaze.base.model.primitives.ExecutionRootPath;
+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.sync.workspace.ArtifactLocationDecoder;
+import com.google.idea.blaze.base.sync.workspace.ArtifactLocationDecoderImpl;
+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 com.intellij.execution.RunnerAndConfigurationSettings;
+import com.intellij.execution.configurations.ConfigurationType;
+import com.intellij.execution.configurations.RunConfiguration;
+import com.intellij.execution.impl.RunManagerImpl;
+import com.intellij.openapi.util.InvalidDataException;
+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.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/**
+ * Test that {@link RunConfigurationSerializer} serializes/deserializes run configurations
+ * correctly.
+ */
+@RunWith(JUnit4.class)
+public class RunConfigurationSerializerTest extends BlazeIntegrationTestCase {
+
+  private RunManagerImpl runManager;
+  private BlazeCommandRunConfigurationType type;
+  private BlazeCommandRunConfiguration configuration;
+
+  @Before
+  public final void doSetup() {
+    runManager = RunManagerImpl.getInstanceImpl(getProject());
+    // Without BlazeProjectData, the configuration editor is always disabled.
+    mockBlazeProjectDataManager(getMockBlazeProjectData());
+    type = BlazeCommandRunConfigurationType.getInstance();
+
+    RunnerAndConfigurationSettings runnerAndConfigurationSettings =
+        runManager.createConfiguration("Blaze Configuration", type.getFactory());
+    runManager.addConfiguration(runnerAndConfigurationSettings, false);
+    configuration =
+        (BlazeCommandRunConfiguration) runnerAndConfigurationSettings.getConfiguration();
+  }
+
+  private BlazeProjectData getMockBlazeProjectData() {
+    BlazeRoots fakeRoots =
+        new BlazeRoots(
+            null,
+            ImmutableList.of(workspaceRoot.directory()),
+            new ExecutionRootPath("out/crosstool/bin"),
+            new ExecutionRootPath("out/crosstool/gen"),
+            null);
+    WorkspacePathResolver workspacePathResolver =
+        new WorkspacePathResolverImpl(workspaceRoot, fakeRoots);
+    ArtifactLocationDecoder artifactLocationDecoder =
+        new ArtifactLocationDecoderImpl(fakeRoots, workspacePathResolver);
+    return new BlazeProjectData(
+        0,
+        new TargetMap(ImmutableMap.of()),
+        ImmutableMap.of(),
+        fakeRoots,
+        new BlazeVersionData(),
+        workspacePathResolver,
+        artifactLocationDecoder,
+        null,
+        null,
+        null);
+  }
+
+  @After
+  public final void doTeardown() {
+    clearRunManager();
+  }
+
+  private void clearRunManager() {
+    runManager.clearAll();
+    // We don't need to do this at setup, because it is handled by RunManagerImpl's constructor.
+    // However, clearAll() clears the configuration types, so we need to reinitialize them.
+    runManager.initializeConfigurationTypes(
+        ConfigurationType.CONFIGURATION_TYPE_EP.getExtensions());
+  }
+
+  @Test
+  public void testRunConfigurationUnalteredBySerializationRoundTrip() throws InvalidDataException {
+    configuration.setTarget(new Label("//package:rule"));
+    configuration.setKeepInSync(true);
+
+    final Element initialElement = runManager.getState();
+
+    Element element = RunConfigurationSerializer.writeToXml(configuration);
+    assertThat(RunConfigurationSerializer.findExisting(getProject(), element)).isNotNull();
+
+    clearRunManager(); // remove configuration from project
+    RunConfigurationSerializer.loadFromXmlElementIgnoreExisting(getProject(), element);
+
+    final Element newElement = runManager.getState();
+    final XMLOutputter xmlOutputter = new XMLOutputter(Format.getCompactFormat());
+    assertThat(xmlOutputter.outputString(newElement))
+        .isEqualTo(xmlOutputter.outputString(initialElement));
+  }
+
+  @Test
+  public void testSetKeepInSyncWhenImporting() throws InvalidDataException {
+    configuration.setTarget(new Label("//package:rule"));
+    configuration.setKeepInSync(false);
+
+    Element element = RunConfigurationSerializer.writeToXml(configuration);
+    assertThat(RunConfigurationSerializer.findExisting(getProject(), element)).isNotNull();
+
+    clearRunManager(); // remove configuration from project
+    RunConfigurationSerializer.loadFromXmlElementIgnoreExisting(getProject(), element);
+
+    RunConfiguration config = runManager.getAllConfigurations()[0];
+    assertThat(config).isInstanceOf(BlazeCommandRunConfiguration.class);
+    assertThat(((BlazeCommandRunConfiguration) config).getKeepInSync()).isTrue();
+  }
+
+  @Test
+  public void testKeepInSyncRespectedWhenImporting() throws InvalidDataException {
+    Element element = RunConfigurationSerializer.writeToXml(configuration);
+
+    configuration.setKeepInSync(false);
+    assertThat(RunConfigurationSerializer.shouldLoadConfiguration(getProject(), element)).isFalse();
+
+    configuration.setKeepInSync(true);
+    assertThat(RunConfigurationSerializer.shouldLoadConfiguration(getProject(), element)).isTrue();
+  }
+}
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
new file mode 100644
index 0000000..e86bd3f
--- /dev/null
+++ b/base/tests/unittests/com/google/idea/blaze/base/actions/BlazeBuildServiceTest.java
@@ -0,0 +1,145 @@
+/*
+ * 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.actions;
+
+import static com.google.common.truth.Truth.assertThat;
+import static org.mockito.Mockito.any;
+import static org.mockito.Mockito.doNothing;
+import static org.mockito.Mockito.eq;
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.verify;
+
+import com.google.common.collect.ImmutableCollection;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Lists;
+import com.google.idea.blaze.base.BlazeTestCase;
+import com.google.idea.blaze.base.model.BlazeProjectData;
+import com.google.idea.blaze.base.model.primitives.Label;
+import com.google.idea.blaze.base.model.primitives.TargetExpression;
+import com.google.idea.blaze.base.projectview.ProjectView;
+import com.google.idea.blaze.base.projectview.ProjectViewManager;
+import com.google.idea.blaze.base.projectview.ProjectViewSet;
+import com.google.idea.blaze.base.projectview.section.ListSection;
+import com.google.idea.blaze.base.projectview.section.sections.TargetSection;
+import com.google.idea.blaze.base.scope.BlazeContext;
+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.BlazeSyncPlugin.ModuleEditor;
+import com.google.idea.blaze.base.sync.data.BlazeProjectDataManager;
+import com.google.idea.blaze.base.sync.workspace.WorkspacePathResolver;
+import java.io.File;
+import java.util.List;
+import javax.annotation.Nullable;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** Test cases for {@link BlazeBuildService}. */
+@RunWith(JUnit4.class)
+public class BlazeBuildServiceTest extends BlazeTestCase {
+  BlazeBuildService service;
+  ProjectViewSet viewSet;
+
+  @Override
+  protected void initTest(Container applicationServices, Container projectServices) {
+    BlazeImportSettingsManager importSettingsManager = new BlazeImportSettingsManager(project);
+    importSettingsManager.setImportSettings(
+        new BlazeImportSettings("", "", "", "", "", Blaze.BuildSystem.Blaze));
+    projectServices.register(BlazeImportSettingsManager.class, importSettingsManager);
+
+    ProjectView view =
+        ProjectView.builder()
+            .add(
+                ListSection.builder(TargetSection.KEY)
+                    .add(TargetExpression.fromString("//view/target:one"))
+                    .add(TargetExpression.fromString("//view/target:two")))
+            .build();
+    viewSet = ProjectViewSet.builder().add(new File("view/target/.blazeproject"), view).build();
+    ProjectViewManager viewManager = new MockProjectViewManager(viewSet);
+    projectServices.register(ProjectViewManager.class, viewManager);
+
+    projectServices.register(BlazeProjectDataManager.class, new MockProjectDataManager());
+
+    applicationServices.register(BlazeBuildService.class, spy(new BlazeBuildService()));
+
+    service = BlazeBuildService.getInstance();
+    assertThat(service).isNotNull();
+
+    // Can't mock BlazeExecutor.submitTask.
+    doNothing().when(service).buildTargetExpressions(any(), any(), any(), any(), any());
+  }
+
+  @Test
+  public void testBuildFile() {
+    ImmutableCollection<Label> labels =
+        ImmutableList.of(new Label("//foo:bar"), new Label("//foo:baz"));
+    List<TargetExpression> targets = Lists.newArrayList(labels);
+    service.buildFile(project, "Foo.java", labels);
+    verify(service).buildTargetExpressions(eq(project), eq(targets), eq(viewSet), any(), any());
+  }
+
+  @Test
+  public void testBuildProject() {
+    service.buildProject(project);
+    List<TargetExpression> targets =
+        Lists.newArrayList(
+            TargetExpression.fromString("//view/target:one"),
+            TargetExpression.fromString("//view/target:two"));
+    verify(service).buildTargetExpressions(eq(project), eq(targets), eq(viewSet), any(), any());
+  }
+
+  private static class MockProjectViewManager extends ProjectViewManager {
+    private final ProjectViewSet viewSet;
+
+    public MockProjectViewManager(ProjectViewSet viewSet) {
+      this.viewSet = viewSet;
+    }
+
+    @Nullable
+    @Override
+    public ProjectViewSet getProjectViewSet() {
+      return viewSet;
+    }
+
+    @Nullable
+    @Override
+    public ProjectViewSet reloadProjectView(
+        BlazeContext context, WorkspacePathResolver workspacePathResolver) {
+      return viewSet;
+    }
+  }
+
+  private static class MockProjectDataManager implements BlazeProjectDataManager {
+    private final BlazeProjectData projectData;
+
+    public MockProjectDataManager() {
+      this.projectData =
+          new BlazeProjectData(0L, null, null, null, null, null, null, null, null, null);
+    }
+
+    @Nullable
+    @Override
+    public BlazeProjectData getBlazeProjectData() {
+      return projectData;
+    }
+
+    @Override
+    public ModuleEditor editModules() {
+      return null;
+    }
+  }
+}
diff --git a/base/tests/unittests/com/google/idea/blaze/base/bazel/BazelVersionTest.java b/base/tests/unittests/com/google/idea/blaze/base/bazel/BazelVersionTest.java
new file mode 100644
index 0000000..5860e40
--- /dev/null
+++ b/base/tests/unittests/com/google/idea/blaze/base/bazel/BazelVersionTest.java
@@ -0,0 +1,71 @@
+/*
+ * 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.bazel;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** Unit tests for {@link BazelVersion}. */
+@RunWith(JUnit4.class)
+public class BazelVersionTest {
+
+  @Test
+  public void testParseOldVersionFormat() {
+    BazelVersion version = BazelVersion.parseVersion("release 0.4.1");
+    assertThat(version).isNotNull();
+    assertThat(version.major).isEqualTo(0);
+    assertThat(version.minor).isEqualTo(4);
+    assertThat(version.bugfix).isEqualTo(1);
+  }
+
+  @Test
+  public void testParseVersionFormatDistributionPackage() {
+    BazelVersion version = BazelVersion.parseVersion("release 0.4.3- (@non-git)");
+    assertThat(version).isNotNull();
+    assertThat(version.major).isEqualTo(0);
+    assertThat(version.minor).isEqualTo(4);
+    assertThat(version.bugfix).isEqualTo(3);
+  }
+
+  @Test
+  public void testParseVersionFormatManualBuild() {
+    BazelVersion version = BazelVersion.parseVersion("release 0.4.3- (@c9139896");
+    assertThat(version).isNotNull();
+    assertThat(version.major).isEqualTo(0);
+    assertThat(version.minor).isEqualTo(4);
+    assertThat(version.bugfix).isEqualTo(3);
+  }
+
+  @Test
+  public void testParseVersionFormatManualOld() {
+    BazelVersion version = BazelVersion.parseVersion("development version");
+    assertThat(version).isEqualTo(BazelVersion.UNKNOWN);
+  }
+
+  @Test
+  public void testIsAtLeast() {
+    BazelVersion version = BazelVersion.parseVersion("release 0.4.1");
+    assertThat(version).isNotNull();
+    assertThat(version.isAtLeast(0, 3, 2)).isTrue();
+    assertThat(version.isAtLeast(0, 4, 0)).isTrue();
+    assertThat(version.isAtLeast(0, 4, 1)).isTrue();
+    assertThat(version.isAtLeast(0, 4, 2)).isFalse();
+    assertThat(version.isAtLeast(0, 5, 0)).isFalse();
+  }
+}
diff --git a/base/tests/unittests/com/google/idea/blaze/base/projectview/ProjectViewSetTest.java b/base/tests/unittests/com/google/idea/blaze/base/projectview/ProjectViewSetTest.java
index 40ea19f..ed1fd3a 100644
--- a/base/tests/unittests/com/google/idea/blaze/base/projectview/ProjectViewSetTest.java
+++ b/base/tests/unittests/com/google/idea/blaze/base/projectview/ProjectViewSetTest.java
@@ -39,6 +39,7 @@
 import com.google.idea.blaze.base.projectview.section.sections.ImportSection;
 import com.google.idea.blaze.base.projectview.section.sections.ImportTargetOutputSection;
 import com.google.idea.blaze.base.projectview.section.sections.MetricsProjectSection;
+import com.google.idea.blaze.base.projectview.section.sections.RunConfigurationsSection;
 import com.google.idea.blaze.base.projectview.section.sections.Sections;
 import com.google.idea.blaze.base.projectview.section.sections.TargetSection;
 import com.google.idea.blaze.base.projectview.section.sections.TestSourceSection;
@@ -91,6 +92,9 @@
                         ListSection.builder(AdditionalLanguagesSection.KEY).add(LanguageClass.JAVA))
                     .add(ScalarSection.builder(MetricsProjectSection.KEY).set("my project"))
                     .add(TextBlockSection.of(TextBlock.newLine()))
+                    .add(
+                        ListSection.builder(RunConfigurationsSection.KEY)
+                            .add(new WorkspacePath("test")))
                     .build())
             .build();
 
diff --git a/base/tests/unittests/com/google/idea/blaze/base/projectview/ProjectViewVerifierTest.java b/base/tests/unittests/com/google/idea/blaze/base/projectview/ProjectViewVerifierTest.java
index 4d27c8e..fe15957 100644
--- a/base/tests/unittests/com/google/idea/blaze/base/projectview/ProjectViewVerifierTest.java
+++ b/base/tests/unittests/com/google/idea/blaze/base/projectview/ProjectViewVerifierTest.java
@@ -33,6 +33,8 @@
 import com.google.idea.blaze.base.sync.BlazeSyncPlugin;
 import com.google.idea.blaze.base.sync.projectview.ImportRoots;
 import com.google.idea.blaze.base.sync.projectview.WorkspaceLanguageSettings;
+import com.google.idea.blaze.base.sync.workspace.WorkspacePathResolver;
+import com.google.idea.blaze.base.sync.workspace.WorkspacePathResolverImpl;
 import java.io.File;
 import java.util.Set;
 import org.jetbrains.annotations.NotNull;
@@ -46,6 +48,8 @@
 
   private static final String FAKE_ROOT = "/root";
   private WorkspaceRoot workspaceRoot = new WorkspaceRoot(new File(FAKE_ROOT));
+  private WorkspacePathResolver workspacePathResolver =
+      new WorkspacePathResolverImpl(workspaceRoot);
   private MockFileAttributeProvider fileAttributeProvider;
   private ErrorCollector errorCollector = new ErrorCollector();
   private BlazeContext context;
@@ -82,7 +86,7 @@
             .build();
     fileAttributeProvider.addProjectView(projectViewSet);
     ProjectViewVerifier.verifyProjectView(
-        context, workspaceRoot, projectViewSet, workspaceLanguageSettings);
+        context, workspacePathResolver, projectViewSet, workspaceLanguageSettings);
     errorCollector.assertNoIssues();
   }
 
@@ -104,7 +108,7 @@
             .build();
     fileAttributeProvider.addProjectView(projectViewSet);
     ProjectViewVerifier.verifyProjectView(
-        context, workspaceRoot, projectViewSet, workspaceLanguageSettings);
+        context, workspacePathResolver, projectViewSet, workspaceLanguageSettings);
     errorCollector.assertIssues(
         "java/com/google/android/apps/example is included, "
             + "but that contradicts java/com/google/android/apps/example which was excluded");
@@ -128,7 +132,7 @@
             .build();
     fileAttributeProvider.addProjectView(projectViewSet);
     ProjectViewVerifier.verifyProjectView(
-        context, workspaceRoot, projectViewSet, workspaceLanguageSettings);
+        context, workspacePathResolver, projectViewSet, workspaceLanguageSettings);
     errorCollector.assertIssues(
         "java/com/google/android/apps/example is included, "
             + "but that contradicts java/com/google/android/apps which was excluded");
@@ -153,7 +157,7 @@
             .build();
     fileAttributeProvider.addProjectView(projectViewSet);
     ProjectViewVerifier.verifyProjectView(
-        context, workspaceRoot, projectViewSet, workspaceLanguageSettings);
+        context, workspacePathResolver, projectViewSet, workspaceLanguageSettings);
     errorCollector.assertNoIssues();
   }
 
@@ -171,11 +175,33 @@
                     .build())
             .build();
     ProjectViewVerifier.verifyProjectView(
-        context, workspaceRoot, projectViewSet, workspaceLanguageSettings);
+        context, workspacePathResolver, projectViewSet, workspaceLanguageSettings);
     errorCollector.assertIssues(
         String.format(
-            "Directory '%s' specified in import roots not found under workspace root '%s'",
-            "java/com/google/android/apps/example", "/root"));
+            "Directory '%s' specified in project view not found.",
+            "java/com/google/android/apps/example"));
+  }
+
+  @Test
+  public void testImportRootIsFileResultsInIssue() {
+    ProjectViewSet projectViewSet =
+        ProjectViewSet.builder()
+            .add(
+                ProjectView.builder()
+                    .add(
+                        ListSection.builder(DirectorySection.KEY)
+                            .add(
+                                DirectoryEntry.include(
+                                    new WorkspacePath("java/com/google/android/apps/example"))))
+                    .build())
+            .build();
+    fileAttributeProvider.addFile(new WorkspacePath("java/com/google/android/apps/example"));
+    ProjectViewVerifier.verifyProjectView(
+        context, workspacePathResolver, projectViewSet, workspaceLanguageSettings);
+    errorCollector.assertIssues(
+        String.format(
+            "Directory '%s' specified in project view is a file.",
+            "java/com/google/android/apps/example"));
   }
 
   static class MockFileAttributeProvider extends FileAttributeProvider {
@@ -228,5 +254,10 @@
     public boolean exists(File file) {
       return files.contains(file);
     }
+
+    @Override
+    public boolean isDirectory(File file) {
+      return directories.contains(file);
+    }
   }
 }
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 c8eaeec..5598b49 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
@@ -21,6 +21,7 @@
 import com.google.common.collect.Lists;
 import com.google.idea.blaze.base.BlazeTestCase;
 import com.google.idea.blaze.base.command.BlazeCommandName;
+import com.google.idea.blaze.base.run.DistributedExecutorSupport;
 import com.google.idea.blaze.base.settings.Blaze;
 import com.google.idea.blaze.base.settings.BlazeImportSettings;
 import com.google.idea.blaze.base.settings.BlazeImportSettingsManager;
@@ -28,7 +29,6 @@
 import org.jdom.Element;
 import org.jdom.output.Format;
 import org.jdom.output.XMLOutputter;
-import org.jetbrains.annotations.NotNull;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.junit.runners.JUnit4;
@@ -43,8 +43,7 @@
   private BlazeCommandRunConfigurationCommonState state;
 
   @Override
-  protected void initTest(
-      @NotNull Container applicationServices, @NotNull Container projectServices) {
+  protected void initTest(Container applicationServices, Container projectServices) {
     super.initTest(applicationServices, projectServices);
 
     applicationServices.register(UISettings.class, new UISettings());
@@ -52,7 +51,9 @@
         BlazeImportSettingsManager.class, new BlazeImportSettingsManager(project));
     BlazeImportSettingsManager.getInstance(getProject()).setImportSettings(DUMMY_IMPORT_SETTINGS);
 
-    state = new BlazeCommandRunConfigurationCommonState(Blaze.buildSystemName(project));
+    registerExtensionPoint(DistributedExecutorSupport.EP_NAME, DistributedExecutorSupport.class);
+
+    state = new BlazeCommandRunConfigurationCommonState(Blaze.getBuildSystem(project));
   }
 
   @Test
@@ -65,7 +66,7 @@
     Element element = new Element("test");
     state.writeExternal(element);
     BlazeCommandRunConfigurationCommonState readState =
-        new BlazeCommandRunConfigurationCommonState(Blaze.buildSystemName(project));
+        new BlazeCommandRunConfigurationCommonState(Blaze.getBuildSystem(project));
     readState.readExternal(element);
 
     assertThat(readState.getCommand()).isEqualTo(COMMAND);
@@ -79,7 +80,7 @@
     Element element = new Element("test");
     state.writeExternal(element);
     BlazeCommandRunConfigurationCommonState readState =
-        new BlazeCommandRunConfigurationCommonState(Blaze.buildSystemName(project));
+        new BlazeCommandRunConfigurationCommonState(Blaze.getBuildSystem(project));
     readState.readExternal(element);
 
     assertThat(readState.getCommand()).isEqualTo(state.getCommand());
@@ -96,7 +97,7 @@
     Element element = new Element("test");
     state.writeExternal(element);
     BlazeCommandRunConfigurationCommonState readState =
-        new BlazeCommandRunConfigurationCommonState(Blaze.buildSystemName(project));
+        new BlazeCommandRunConfigurationCommonState(Blaze.getBuildSystem(project));
     readState.readExternal(element);
 
     assertThat(readState.getBlazeFlags()).containsExactly("hi", "I'm", "Josh").inOrder();
@@ -132,7 +133,7 @@
 
     editor.resetEditorFrom(state);
     BlazeCommandRunConfigurationCommonState readState =
-        new BlazeCommandRunConfigurationCommonState(Blaze.buildSystemName(project));
+        new BlazeCommandRunConfigurationCommonState(Blaze.getBuildSystem(project));
     editor.applyEditorTo(readState);
 
     assertThat(readState.getCommand()).isEqualTo(state.getCommand());
@@ -147,7 +148,7 @@
 
     editor.resetEditorFrom(state);
     BlazeCommandRunConfigurationCommonState readState =
-        new BlazeCommandRunConfigurationCommonState(Blaze.buildSystemName(project));
+        new BlazeCommandRunConfigurationCommonState(Blaze.getBuildSystem(project));
     editor.applyEditorTo(readState);
 
     assertThat(readState.getCommand()).isEqualTo(state.getCommand());
diff --git a/base/tests/unittests/com/google/idea/blaze/base/run/state/RunConfigurationFlagStateTest.java b/base/tests/unittests/com/google/idea/blaze/base/run/state/RunConfigurationFlagStateTest.java
new file mode 100644
index 0000000..730ac3b
--- /dev/null
+++ b/base/tests/unittests/com/google/idea/blaze/base/run/state/RunConfigurationFlagStateTest.java
@@ -0,0 +1,75 @@
+/*
+ * 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.run.state;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.google.common.collect.ImmutableList;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** Unit tests for {@link RunConfigurationFlagsState}. */
+@RunWith(JUnit4.class)
+public class RunConfigurationFlagStateTest {
+
+  @Test
+  public void testEscapedQuotesRetainedAfterReserialization() {
+    // previously, we were removing escape chars and quotes during ParametersListUtil.parse, then
+    // not putting them back when converting back to a string.
+    ImmutableList<String> flags = ImmutableList.of("--flag=\\\"Hello_world!\\\"", "--flag2");
+    RunConfigurationFlagsState state = new RunConfigurationFlagsState("tag", "field");
+    state.setFlags(flags);
+
+    RunConfigurationStateEditor editor = state.getEditor(null);
+    editor.resetEditorFrom(state);
+    editor.applyEditorTo(state);
+
+    assertThat(state.getFlags()).isEqualTo(flags);
+  }
+
+  @Test
+  public void testQuotesRetainedAfterReserialization() {
+    ImmutableList<String> flags = ImmutableList.of("\"--flag=test\"");
+    RunConfigurationFlagsState state = new RunConfigurationFlagsState("tag", "field");
+    state.setFlags(flags);
+
+    RunConfigurationStateEditor editor = state.getEditor(null);
+    editor.resetEditorFrom(state);
+    editor.applyEditorTo(state);
+
+    assertThat(state.getFlags()).isEqualTo(flags);
+  }
+
+  @Test
+  public void testNormalFlagsAreNotMangled() {
+    ImmutableList<String> flags =
+        ImmutableList.of(
+            "--test_sharding_strategy=disabled",
+            "--test_strategy=local",
+            "--experimental_show_artifacts",
+            "--test_filter=com.google.idea.blaze.base.run.state.RunConfigurationFlagStateTest#",
+            "--define=ij_product=intellij-latest");
+    RunConfigurationFlagsState state = new RunConfigurationFlagsState("tag", "field");
+    state.setFlags(flags);
+
+    RunConfigurationStateEditor editor = state.getEditor(null);
+    editor.resetEditorFrom(state);
+    editor.applyEditorTo(state);
+
+    assertThat(state.getFlags()).isEqualTo(flags);
+  }
+}
diff --git a/base/tests/unittests/com/google/idea/blaze/base/sync/BlazeSyncManagerTest.java b/base/tests/unittests/com/google/idea/blaze/base/sync/BlazeSyncManagerTest.java
new file mode 100644
index 0000000..c8cf5ba
--- /dev/null
+++ b/base/tests/unittests/com/google/idea/blaze/base/sync/BlazeSyncManagerTest.java
@@ -0,0 +1,115 @@
+/*
+ * 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;
+
+import static com.google.common.truth.Truth.assertThat;
+import static org.mockito.Matchers.any;
+import static org.mockito.Mockito.doNothing;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+
+import com.google.common.collect.ImmutableList;
+import com.google.idea.blaze.base.BlazeTestCase;
+import com.google.idea.blaze.base.model.primitives.TargetExpression;
+import com.google.idea.blaze.base.settings.BlazeUserSettings;
+import com.google.idea.blaze.base.sync.BlazeSyncParams.SyncMode;
+import java.io.IOException;
+import java.util.List;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Captor;
+import org.mockito.MockitoAnnotations;
+import org.mockito.Spy;
+
+/** Test cases for {@link BlazeSyncManager}. */
+@RunWith(JUnit4.class)
+public class BlazeSyncManagerTest extends BlazeTestCase {
+  @Spy BlazeSyncManager manager = new BlazeSyncManager(project);
+  @Captor ArgumentCaptor<BlazeSyncParams> paramsCaptor;
+
+  @Override
+  protected void initTest(Container applicationServices, Container projectServices) {
+    super.initTest(applicationServices, projectServices);
+    MockitoAnnotations.initMocks(this);
+    applicationServices.register(BlazeUserSettings.class, mock(BlazeUserSettings.class));
+    doNothing().when(manager).requestProjectSync(any());
+    projectServices.register(BlazeSyncManager.class, manager);
+    assertThat(BlazeSyncManager.getInstance(project)).isSameAs(manager);
+  }
+
+  @Test
+  public void testFullProjectSync() throws IOException {
+    manager.fullProjectSync();
+    verify(manager).requestProjectSync(paramsCaptor.capture());
+    BlazeSyncParams params = paramsCaptor.getValue();
+    assertThat(params).isNotNull();
+    assertThat(params.title).isEqualTo("Full Sync");
+    assertThat(params.syncMode).isEqualTo(SyncMode.FULL);
+    assertThat(params.backgroundSync).isFalse();
+    assertThat(params.addProjectViewTargets).isTrue();
+    assertThat(params.addWorkingSet)
+        .isEqualTo(BlazeUserSettings.getInstance().getExpandSyncToWorkingSet());
+    assertThat(params.targetExpressions).isEmpty();
+  }
+
+  @Test
+  public void testIncrementalProjectSync() throws IOException {
+    manager.incrementalProjectSync();
+    verify(manager).requestProjectSync(paramsCaptor.capture());
+    BlazeSyncParams params = paramsCaptor.getValue();
+    assertThat(params).isNotNull();
+    assertThat(params.title).isEqualTo("Sync");
+    assertThat(params.syncMode).isEqualTo(SyncMode.INCREMENTAL);
+    assertThat(params.backgroundSync).isFalse();
+    assertThat(params.addProjectViewTargets).isTrue();
+    assertThat(params.addWorkingSet)
+        .isEqualTo(BlazeUserSettings.getInstance().getExpandSyncToWorkingSet());
+    assertThat(params.targetExpressions).isEmpty();
+  }
+
+  @Test
+  public void testPartialSync() {
+    List<TargetExpression> targets =
+        ImmutableList.of(
+            TargetExpression.fromString("//foo:bar"), TargetExpression.fromString("//foo:baz"));
+    manager.partialSync(targets);
+    verify(manager).requestProjectSync(paramsCaptor.capture());
+    BlazeSyncParams params = paramsCaptor.getValue();
+    assertThat(params).isNotNull();
+    assertThat(params.title).isEqualTo("Partial Sync");
+    assertThat(params.syncMode).isEqualTo(SyncMode.PARTIAL);
+    assertThat(params.backgroundSync).isFalse();
+    assertThat(params.addProjectViewTargets).isFalse();
+    assertThat(params.addWorkingSet).isFalse();
+    assertThat(params.targetExpressions).isEqualTo(targets);
+  }
+
+  @Test
+  public void testWorkingSetSync() throws IOException {
+    manager.workingSetSync();
+    verify(manager).requestProjectSync(paramsCaptor.capture());
+    BlazeSyncParams params = paramsCaptor.getValue();
+    assertThat(params).isNotNull();
+    assertThat(params.title).isEqualTo("Sync Working Set");
+    assertThat(params.syncMode).isEqualTo(SyncMode.PARTIAL);
+    assertThat(params.backgroundSync).isFalse();
+    assertThat(params.addProjectViewTargets).isFalse();
+    assertThat(params.addWorkingSet).isTrue();
+    assertThat(params.targetExpressions).isEmpty();
+  }
+}
diff --git a/base/tests/unittests/com/google/idea/blaze/base/targetmaps/TransitiveDependencyMapTest.java b/base/tests/unittests/com/google/idea/blaze/base/targetmaps/TransitiveDependencyMapTest.java
new file mode 100644
index 0000000..3037ff1
--- /dev/null
+++ b/base/tests/unittests/com/google/idea/blaze/base/targetmaps/TransitiveDependencyMapTest.java
@@ -0,0 +1,167 @@
+/*
+ * 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.targetmaps;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.google.idea.blaze.base.BlazeTestCase;
+import com.google.idea.blaze.base.ideinfo.TargetIdeInfo;
+import com.google.idea.blaze.base.ideinfo.TargetKey;
+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.Label;
+import com.google.idea.blaze.base.sync.BlazeSyncPlugin.ModuleEditor;
+import com.google.idea.blaze.base.sync.data.BlazeProjectDataManager;
+import javax.annotation.Nullable;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** Unit tests for {@link TransitiveDependencyMap}. */
+@RunWith(JUnit4.class)
+public class TransitiveDependencyMapTest extends BlazeTestCase {
+  private TransitiveDependencyMap transitiveDependencyMap;
+
+  @Override
+  protected void initTest(Container applicationServices, Container projectServices) {
+    super.initTest(applicationServices, projectServices);
+    projectServices.register(
+        BlazeProjectDataManager.class,
+        new MockBlazeProjectDataManager(
+            new BlazeProjectData(
+                0L, buildTargetMap(), null, null, null, null, null, null, null, null)));
+    projectServices.register(TransitiveDependencyMap.class, new TransitiveDependencyMap(project));
+    transitiveDependencyMap = TransitiveDependencyMap.getInstance(project);
+  }
+
+  @Test
+  public void testGetSimpleDependency() {
+    TargetKey simpleA = TargetKey.forPlainTarget(new Label("//com/google/example/simple:a"));
+    TargetKey simpleB = TargetKey.forPlainTarget(new Label("//com/google/example/simple:b"));
+
+    assertThat(transitiveDependencyMap.getTransitiveDependencies(simpleA)).containsExactly(simpleB);
+    assertThat(transitiveDependencyMap.getTransitiveDependencies(simpleB)).isEmpty();
+  }
+
+  @Test
+  public void testGetChainDependencies() {
+    TargetKey chainA = TargetKey.forPlainTarget(new Label("//com/google/example/chain:a"));
+    TargetKey chainB = TargetKey.forPlainTarget(new Label("//com/google/example/chain:b"));
+    TargetKey chainC = TargetKey.forPlainTarget(new Label("//com/google/example/chain:c"));
+    TargetKey chainD = TargetKey.forPlainTarget(new Label("//com/google/example/chain:d"));
+
+    assertThat(transitiveDependencyMap.getTransitiveDependencies(chainA))
+        .containsExactly(chainB, chainC, chainD);
+    assertThat(transitiveDependencyMap.getTransitiveDependencies(chainB))
+        .containsExactly(chainC, chainD);
+    assertThat(transitiveDependencyMap.getTransitiveDependencies(chainC)).containsExactly(chainD);
+    assertThat(transitiveDependencyMap.getTransitiveDependencies(chainD)).isEmpty();
+  }
+
+  @Test
+  public void testGetDiamondDependencies() {
+    TargetKey diamondA = TargetKey.forPlainTarget(new Label("//com/google/example/diamond:a"));
+    TargetKey diamondB = TargetKey.forPlainTarget(new Label("//com/google/example/diamond:b"));
+    TargetKey diamondBB = TargetKey.forPlainTarget(new Label("//com/google/example/diamond:bb"));
+    TargetKey diamondBBB = TargetKey.forPlainTarget(new Label("//com/google/example/diamond:bbb"));
+    TargetKey diamondC = TargetKey.forPlainTarget(new Label("//com/google/example/diamond:c"));
+    TargetKey diamondCC = TargetKey.forPlainTarget(new Label("//com/google/example/diamond:cc"));
+    TargetKey diamondCCC = TargetKey.forPlainTarget(new Label("//com/google/example/diamond:ccc"));
+
+    assertThat(transitiveDependencyMap.getTransitiveDependencies(diamondA))
+        .containsExactly(diamondB, diamondBB, diamondBBB, diamondC, diamondCC, diamondCCC);
+    assertThat(transitiveDependencyMap.getTransitiveDependencies(diamondB))
+        .containsExactly(diamondC);
+    assertThat(transitiveDependencyMap.getTransitiveDependencies(diamondBB))
+        .containsExactly(diamondC, diamondCC);
+    assertThat(transitiveDependencyMap.getTransitiveDependencies(diamondBBB))
+        .containsExactly(diamondC, diamondCC, diamondCCC);
+    assertThat(transitiveDependencyMap.getTransitiveDependencies(diamondC)).isEmpty();
+    assertThat(transitiveDependencyMap.getTransitiveDependencies(diamondCC)).isEmpty();
+    assertThat(transitiveDependencyMap.getTransitiveDependencies(diamondCCC)).isEmpty();
+  }
+
+  @Test
+  public void testGetDependencyForNonExistentTarget() {
+    TargetKey bogus = TargetKey.forPlainTarget(new Label("//com/google/fake:target"));
+    assertThat(transitiveDependencyMap.getTransitiveDependencies(bogus)).isEmpty();
+  }
+
+  private static TargetMap buildTargetMap() {
+    Label simpleA = new Label("//com/google/example/simple:a");
+    Label simpleB = new Label("//com/google/example/simple:b");
+    Label chainA = new Label("//com/google/example/chain:a");
+    Label chainB = new Label("//com/google/example/chain:b");
+    Label chainC = new Label("//com/google/example/chain:c");
+    Label chainD = new Label("//com/google/example/chain:d");
+    Label diamondA = new Label("//com/google/example/diamond:a");
+    Label diamondB = new Label("//com/google/example/diamond:b");
+    Label diamondBB = new Label("//com/google/example/diamond:bb");
+    Label diamondBBB = new Label("//com/google/example/diamond:bbb");
+    Label diamondC = new Label("//com/google/example/diamond:c");
+    Label diamondCC = new Label("//com/google/example/diamond:cc");
+    Label diamondCCC = new Label("//com/google/example/diamond:ccc");
+    return TargetMapBuilder.builder()
+        .addTarget(TargetIdeInfo.builder().setLabel(simpleA).addDependency(simpleB))
+        .addTarget(TargetIdeInfo.builder().setLabel(simpleB))
+        .addTarget(TargetIdeInfo.builder().setLabel(chainA).addDependency(chainB))
+        .addTarget(TargetIdeInfo.builder().setLabel(chainB).addDependency(chainC))
+        .addTarget(TargetIdeInfo.builder().setLabel(chainC).addDependency(chainD))
+        .addTarget(TargetIdeInfo.builder().setLabel(chainD))
+        .addTarget(
+            TargetIdeInfo.builder()
+                .setLabel(diamondA)
+                .addDependency(diamondB)
+                .addDependency(diamondBB)
+                .addDependency(diamondBBB))
+        .addTarget(TargetIdeInfo.builder().setLabel(diamondB).addDependency(diamondC))
+        .addTarget(
+            TargetIdeInfo.builder()
+                .setLabel(diamondBB)
+                .addDependency(diamondC)
+                .addDependency(diamondCC))
+        .addTarget(
+            TargetIdeInfo.builder()
+                .setLabel(diamondBBB)
+                .addDependency(diamondC)
+                .addDependency(diamondCC)
+                .addDependency(diamondCCC))
+        .addTarget(TargetIdeInfo.builder().setLabel(diamondC))
+        .addTarget(TargetIdeInfo.builder().setLabel(diamondCC))
+        .addTarget(TargetIdeInfo.builder().setLabel(diamondCCC))
+        .build();
+  }
+
+  private static class MockBlazeProjectDataManager implements BlazeProjectDataManager {
+    private final BlazeProjectData blazeProjectData;
+
+    public MockBlazeProjectDataManager(BlazeProjectData blazeProjectData) {
+      this.blazeProjectData = blazeProjectData;
+    }
+
+    @Nullable
+    @Override
+    public BlazeProjectData getBlazeProjectData() {
+      return blazeProjectData;
+    }
+
+    @Override
+    public ModuleEditor editModules() {
+      return null;
+    }
+  }
+}
diff --git a/base/tests/unittests/com/google/idea/blaze/base/vcs/git/GitStatusLineProcessorTest.java b/base/tests/unittests/com/google/idea/blaze/base/vcs/git/GitStatusLineProcessorTest.java
index 06cbe96..36fbdc0 100644
--- a/base/tests/unittests/com/google/idea/blaze/base/vcs/git/GitStatusLineProcessorTest.java
+++ b/base/tests/unittests/com/google/idea/blaze/base/vcs/git/GitStatusLineProcessorTest.java
@@ -18,9 +18,11 @@
 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.model.primitives.WorkspacePath;
 import com.google.idea.blaze.base.model.primitives.WorkspaceRoot;
 import java.io.File;
+import org.junit.Rule;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.junit.runners.JUnit4;
@@ -28,6 +30,7 @@
 /** Tests for {@link GitStatusLineProcessor} */
 @RunWith(JUnit4.class)
 public class GitStatusLineProcessorTest {
+  @Rule public BlazeTestCase.IgnoreOnWindowsRule rule = new BlazeTestCase.IgnoreOnWindowsRule();
 
   @Test
   public void testGitStatusParser() {
diff --git a/base/tests/utils/integration/com/google/idea/blaze/base/BlazeIntegrationTestCase.java b/base/tests/utils/integration/com/google/idea/blaze/base/BlazeIntegrationTestCase.java
index 633c9df..81d3e75 100644
--- a/base/tests/utils/integration/com/google/idea/blaze/base/BlazeIntegrationTestCase.java
+++ b/base/tests/utils/integration/com/google/idea/blaze/base/BlazeIntegrationTestCase.java
@@ -34,6 +34,7 @@
 import com.intellij.openapi.extensions.ExtensionPointName;
 import com.intellij.openapi.project.Project;
 import com.intellij.openapi.projectRoots.ProjectJdkTable;
+import com.intellij.openapi.util.SystemInfo;
 import com.intellij.openapi.vfs.VirtualFile;
 import com.intellij.testFramework.EdtTestUtil;
 import com.intellij.testFramework.IdeaTestUtil;
@@ -44,6 +45,7 @@
 import com.intellij.testFramework.fixtures.LightCodeInsightFixtureTestCase;
 import com.intellij.testFramework.fixtures.TestFixtureBuilder;
 import com.intellij.testFramework.fixtures.impl.LightTempDirTestFixtureImpl;
+import com.intellij.util.ThrowableRunnable;
 import java.io.File;
 import java.io.FileNotFoundException;
 import javax.annotation.Nullable;
@@ -51,10 +53,32 @@
 import org.junit.Before;
 import org.junit.Rule;
 import org.junit.rules.TestRule;
+import org.junit.runner.Description;
+import org.junit.runners.model.Statement;
 
 /** Base test class for blaze integration tests. {@link UsefulTestCase} */
 public abstract class BlazeIntegrationTestCase {
 
+  /** Test rule that ensures tests do not run on Windows (see http://b.android.com/222904) */
+  public static class IgnoreOnWindowsRule implements TestRule {
+    @Override
+    public Statement apply(Statement base, Description description) {
+      if (SystemInfo.isWindows) {
+        return new Statement() {
+          @Override
+          public void evaluate() throws Throwable {
+            System.out.println(
+                "Test \""
+                    + description.getDisplayName()
+                    + "\" does not run on Windows (see http://b.android.com/222904)");
+          }
+        };
+      }
+      return base;
+    }
+  }
+
+  @Rule public final IgnoreOnWindowsRule rule = new IgnoreOnWindowsRule();
   @Rule public final IntellijTestSetupRule setupRule = new IntellijTestSetupRule();
   @Rule public final TestRule testRunWrapper = runTestsOnEdt() ? new EdtRule() : null;
 
@@ -74,20 +98,20 @@
     testFixture.setUp();
     fileSystem = new TestFileSystem(getProject(), testFixture.getTempDirFixture());
 
-    Runnable writeAction =
-        () ->
-            ApplicationManager.getApplication()
-                .runWriteAction(
-                    () -> {
-                      ProjectJdkTable.getInstance().addJdk(IdeaTestUtil.getMockJdk18());
-                      VirtualFile workspaceRootVirtualFile =
-                          fileSystem.createDirectory("workspace");
-                      workspaceRoot =
-                          new WorkspaceRoot(new File(workspaceRootVirtualFile.getPath()));
-                      projectDataDirectory = fileSystem.createDirectory("project-data-dir");
-                      workspace = new WorkspaceFileSystem(workspaceRoot, fileSystem);
-                    });
-    EdtTestUtil.runInEdtAndWait(writeAction);
+    EdtTestUtil.runInEdtAndWait(
+        (ThrowableRunnable<Throwable>)
+            () ->
+                ApplicationManager.getApplication()
+                    .runWriteAction(
+                        () -> {
+                          ProjectJdkTable.getInstance().addJdk(IdeaTestUtil.getMockJdk18());
+                          VirtualFile workspaceRootVirtualFile =
+                              fileSystem.createDirectory("workspace");
+                          workspaceRoot =
+                              new WorkspaceRoot(new File(workspaceRootVirtualFile.getPath()));
+                          projectDataDirectory = fileSystem.createDirectory("project-data-dir");
+                          workspace = new WorkspaceFileSystem(workspaceRoot, fileSystem);
+                        }));
 
     BlazeImportSettingsManager.getInstance(getProject())
         .setImportSettings(
diff --git a/base/tests/utils/integration/com/google/idea/blaze/base/EditorTestHelper.java b/base/tests/utils/integration/com/google/idea/blaze/base/EditorTestHelper.java
index e7a2d4e..b4dede2 100644
--- a/base/tests/utils/integration/com/google/idea/blaze/base/EditorTestHelper.java
+++ b/base/tests/utils/integration/com/google/idea/blaze/base/EditorTestHelper.java
@@ -36,6 +36,7 @@
 import com.intellij.testFramework.EditorTestUtil.CaretInfo;
 import com.intellij.testFramework.EdtTestUtil;
 import com.intellij.testFramework.fixtures.CodeInsightTestFixture;
+import com.intellij.util.ThrowableRunnable;
 import java.util.Arrays;
 import javax.annotation.Nullable;
 
@@ -54,7 +55,8 @@
   }
 
   public Editor openFileInEditor(VirtualFile file) {
-    EdtTestUtil.runInEdtAndWait((Runnable) () -> testFixture.openFileInEditor(file));
+    EdtTestUtil.runInEdtAndWait(
+        (ThrowableRunnable<Throwable>) () -> testFixture.openFileInEditor(file));
     return testFixture.getEditor();
   }
 
@@ -125,7 +127,7 @@
   public void setCaretPosition(Editor editor, int lineNumber, int columnNumber) {
     final CaretInfo info = new CaretInfo(new LogicalPosition(lineNumber, columnNumber), null);
     EdtTestUtil.runInEdtAndWait(
-        (Runnable)
+        (ThrowableRunnable<Throwable>)
             () ->
                 EditorTestUtil.setCaretsAndSelection(
                     editor, new CaretAndSelectionState(ImmutableList.of(info), null)));
diff --git a/base/tests/utils/integration/com/google/idea/blaze/base/lang/buildfile/BuildFileIntegrationTestCase.java b/base/tests/utils/integration/com/google/idea/blaze/base/lang/buildfile/BuildFileIntegrationTestCase.java
index efa81a8..78d1ec3 100644
--- a/base/tests/utils/integration/com/google/idea/blaze/base/lang/buildfile/BuildFileIntegrationTestCase.java
+++ b/base/tests/utils/integration/com/google/idea/blaze/base/lang/buildfile/BuildFileIntegrationTestCase.java
@@ -25,12 +25,13 @@
 import com.google.idea.blaze.base.ideinfo.TargetMap;
 import com.google.idea.blaze.base.lang.buildfile.psi.BuildFile;
 import com.google.idea.blaze.base.model.BlazeProjectData;
+import com.google.idea.blaze.base.model.BlazeVersionData;
+import com.google.idea.blaze.base.model.SyncState;
 import com.google.idea.blaze.base.model.primitives.ExecutionRootPath;
 import com.google.idea.blaze.base.model.primitives.WorkspacePath;
 import com.google.idea.blaze.base.sync.workspace.ArtifactLocationDecoder;
 import com.google.idea.blaze.base.sync.workspace.ArtifactLocationDecoderImpl;
 import com.google.idea.blaze.base.sync.workspace.BlazeRoots;
-import com.google.idea.blaze.base.sync.workspace.WorkingSet;
 import com.google.idea.blaze.base.sync.workspace.WorkspacePathResolver;
 import com.google.idea.blaze.base.sync.workspace.WorkspacePathResolverImpl;
 import com.intellij.openapi.vfs.VirtualFile;
@@ -83,12 +84,11 @@
         new TargetMap(ImmutableMap.of()),
         ImmutableMap.of(),
         fakeRoots,
-        new WorkingSet(ImmutableList.of(), ImmutableList.of(), ImmutableList.of()),
+        new BlazeVersionData(),
         workspacePathResolver,
         artifactLocationDecoder,
         null,
-        null,
-        null,
+        new SyncState.Builder().build(),
         null);
   }
 }
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 b197413..a0ac661 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
@@ -29,6 +29,7 @@
 import com.google.idea.blaze.base.command.info.BlazeInfo;
 import com.google.idea.blaze.base.ideinfo.ArtifactLocation;
 import com.google.idea.blaze.base.ideinfo.TargetMap;
+import com.google.idea.blaze.base.model.BlazeVersionData;
 import com.google.idea.blaze.base.model.SyncState;
 import com.google.idea.blaze.base.model.primitives.TargetExpression;
 import com.google.idea.blaze.base.model.primitives.WorkspacePath;
@@ -53,6 +54,7 @@
 import com.google.idea.blaze.base.sync.workspace.WorkspacePathResolver;
 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.project.Project;
 import com.intellij.openapi.roots.ContentEntry;
 import com.intellij.openapi.roots.ModifiableRootModel;
@@ -60,6 +62,8 @@
 import com.intellij.openapi.vfs.VirtualFile;
 import java.util.List;
 import java.util.Map;
+import java.util.concurrent.Executors;
+import java.util.concurrent.Future;
 import javax.annotation.Nullable;
 import org.junit.Before;
 
@@ -182,7 +186,20 @@
             project,
             BlazeImportSettingsManager.getInstance(project).getImportSettings(),
             syncParams);
-    syncTask.syncProject(context);
+
+    // We need to run sync off EDT to keep IntelliJ's transaction system happy
+    // Because the sync task itself wants to run occasional EDT tasks, we'll have
+    // to keep flushing the event queue.
+    Future<?> future =
+        Executors.newSingleThreadExecutor().submit(() -> syncTask.syncProject(context));
+    while (!future.isDone()) {
+      IdeEventQueue.getInstance().flushQueue();
+      try {
+        Thread.sleep(50);
+      } catch (InterruptedException e) {
+        e.printStackTrace();
+      }
+    }
   }
 
   private static class MockProjectViewManager extends ProjectViewManager {
@@ -282,6 +299,7 @@
         BlazeContext context,
         WorkspaceRoot workspaceRoot,
         ProjectViewSet projectViewSet,
+        BlazeVersionData blazeVersionData,
         List<TargetExpression> targets,
         WorkspaceLanguageSettings workspaceLanguageSettings,
         ArtifactLocationDecoder artifactLocationDecoder,
@@ -297,6 +315,18 @@
         BlazeContext context,
         WorkspaceRoot workspaceRoot,
         ProjectViewSet projectViewSet,
+        BlazeVersionData blazeVersionData,
+        List<TargetExpression> targets) {
+      return BuildResult.SUCCESS;
+    }
+
+    @Override
+    public BuildResult compileIdeArtifacts(
+        Project project,
+        BlazeContext context,
+        WorkspaceRoot workspaceRoot,
+        ProjectViewSet projectViewSet,
+        BlazeVersionData blazeVersionData,
         List<TargetExpression> targets) {
       return BuildResult.SUCCESS;
     }
diff --git a/base/tests/utils/unit/com/google/idea/blaze/base/BlazeTestCase.java b/base/tests/utils/unit/com/google/idea/blaze/base/BlazeTestCase.java
index 428b180..874fd28 100644
--- a/base/tests/utils/unit/com/google/idea/blaze/base/BlazeTestCase.java
+++ b/base/tests/utils/unit/com/google/idea/blaze/base/BlazeTestCase.java
@@ -19,17 +19,22 @@
 import com.intellij.openapi.Disposable;
 import com.intellij.openapi.application.ApplicationManager;
 import com.intellij.openapi.extensions.DefaultPluginDescriptor;
-import com.intellij.openapi.extensions.ExtensionPoint;
 import com.intellij.openapi.extensions.ExtensionPointName;
 import com.intellij.openapi.extensions.Extensions;
+import com.intellij.openapi.extensions.PluginDescriptor;
 import com.intellij.openapi.extensions.PluginId;
 import com.intellij.openapi.extensions.impl.ExtensionPointImpl;
 import com.intellij.openapi.extensions.impl.ExtensionsAreaImpl;
 import com.intellij.openapi.project.Project;
 import com.intellij.openapi.util.Disposer;
+import com.intellij.openapi.util.SystemInfo;
 import org.jetbrains.annotations.NotNull;
 import org.junit.After;
 import org.junit.Before;
+import org.junit.Rule;
+import org.junit.rules.TestRule;
+import org.junit.runner.Description;
+import org.junit.runners.model.Statement;
 import org.picocontainer.MutablePicoContainer;
 
 /**
@@ -40,6 +45,27 @@
  * <p>Provides a mock application and a mock project.
  */
 public class BlazeTestCase {
+  /** Test rule that ensures tests do not run on Windows (see http://b.android.com/222904) */
+  public static class IgnoreOnWindowsRule implements TestRule {
+    @NotNull
+    @Override
+    public Statement apply(Statement base, Description description) {
+      if (SystemInfo.isWindows) {
+        return new Statement() {
+          @Override
+          public void evaluate() throws Throwable {
+            System.out.println(
+                "Test \""
+                    + description.getDisplayName()
+                    + "\" does not run on Windows (see http://b.android.com/222904)");
+          }
+        };
+      }
+      return base;
+    }
+  }
+
+  @Rule public IgnoreOnWindowsRule rule = new IgnoreOnWindowsRule();
 
   protected Project project;
   private ExtensionsAreaImpl extensionsArea;
@@ -94,16 +120,9 @@
 
   protected <T> ExtensionPointImpl<T> registerExtensionPoint(
       @NotNull ExtensionPointName<T> name, @NotNull Class<T> type) {
-    ExtensionPointImpl<T> extensionPoint =
-        new ExtensionPointImpl<T>(
-            name.getName(),
-            type.getName(),
-            ExtensionPoint.Kind.INTERFACE,
-            extensionsArea,
-            null,
-            new Extensions.SimpleLogProvider(),
-            new DefaultPluginDescriptor(PluginId.getId(type.getName()), type.getClassLoader()));
-    extensionsArea.registerExtensionPoint(extensionPoint);
-    return extensionPoint;
+    PluginDescriptor pluginDescriptor =
+        new DefaultPluginDescriptor(PluginId.getId(type.getName()), type.getClassLoader());
+    extensionsArea.registerExtensionPoint(name.getName(), type.getName(), pluginDescriptor);
+    return extensionsArea.getExtensionPoint(name.getName());
   }
 }
diff --git a/base/tests/utils/unit/com/google/idea/blaze/base/run/MockBlazeCommandRunConfigurationHandlerProvider.java b/base/tests/utils/unit/com/google/idea/blaze/base/run/MockBlazeCommandRunConfigurationHandlerProvider.java
index 4f30237..c4b6762 100644
--- a/base/tests/utils/unit/com/google/idea/blaze/base/run/MockBlazeCommandRunConfigurationHandlerProvider.java
+++ b/base/tests/utils/unit/com/google/idea/blaze/base/run/MockBlazeCommandRunConfigurationHandlerProvider.java
@@ -81,6 +81,9 @@
         public JComponent createComponent() {
           return null;
         }
+
+        @Override
+        public void setComponentEnabled(boolean enabled) {}
       };
     }
   }
diff --git a/build_defs/build_defs.bzl b/build_defs/build_defs.bzl
index f836204..3104e0f 100644
--- a/build_defs/build_defs.bzl
+++ b/build_defs/build_defs.bzl
@@ -191,22 +191,16 @@
       "chmod +w $@",
       "mkdir -p META-INF",
       "cp $(location {plugin_xml}) META-INF/plugin.xml".format(plugin_xml=plugin_xml),
-      "$(location {zip_tool}) -u $@ META-INF/plugin.xml >/dev/null".format(zip_tool=zip_tool),
   ]
-  srcs = [
+  srcs = meta_inf_files + [
       plugin_xml,
       deploy_jar,
   ]
 
   for meta_inf_file in meta_inf_files:
-    cmd.append("cp $(location {meta_inf_file}) META-INF/$$(basename $(location {meta_inf_file}))".format(
-        meta_inf_file=meta_inf_file,
-    ))
-    cmd.append("$(location {zip_tool}) -u $@ META-INF/$$(basename $(location {meta_inf_file})) >/dev/null".format(
-        zip_tool=zip_tool,
-        meta_inf_file=meta_inf_file,
-    ))
-    srcs.append(meta_inf_file)
+    cmd.append("meta_inf_files='$(locations {meta_inf_file})'".format(meta_inf_file=meta_inf_file))
+    cmd.append("for f in $$meta_inf_files; do cp $$f META-INF/; done")
+  cmd.append("$(location {zip_tool}) -u $@ META-INF/* >/dev/null".format(zip_tool=zip_tool))
 
   native.genrule(
       name = name + "_genrule",
@@ -236,28 +230,38 @@
       tags = ["intellij-plugin-bundle"],
   )
 
-def repackage_jar(name, src_rule, out,
-                  rules = [
-                      "com.google.common.** com.google.repackaged.common.@1",
-                      "com.google.gson.** com.google.repackaged.gson.@1",
-                      "com.google.protobuf.** com.google.repackaged.protobuf.@1",
-                      "com.google.thirdparty.** com.google.repackaged.thirdparty.@1",
-                  ], **kwargs):
+def repackaged_jar(name, deps, rules, launcher=None, **kwargs):
   """Repackages classes in a jar, to avoid collisions in the classpath.
 
   Args:
     name: the name of this target
-    src_rule: a java_binary label with the create_executable attribute set to 0
-    out: the output jarfile
+    deps: The dependencies repackage
     rules: the rules to apply in the repackaging
-        Only repackage some of com.google.** from proto_deps.jar.
-        We do not repackage:
+        Do not repackage:
         - com.google.net.** because that has JNI files which use
           FindClass(JNIEnv *, const char *) with hard-coded native string
           literals that jarjar doesn't rewrite.
         - com.google.errorprone packages (rewriting will throw off blaze build).
+    launcher: The launcher arg to pass to java_binary
     **kwargs: Any additional arguments to pass to the final target.
   """
+  java_binary_name = name + "_deploy_jar"
+  out = name + ".jar"
+  native.java_binary(
+      name = java_binary_name,
+      create_executable = 0,
+      stamp = 0,
+      launcher = launcher,
+      runtime_deps = deps,
+  )
+  _repackage_jar(name, java_binary_name, out, rules, **kwargs)
+
+def repackage_jar(name, src_rule, out, rules, **kwargs):
+  print("repackage_jar is deprecated. Please switch to repackaged_jar.")
+  _repackage_jar(name,  src_rule, out, rules, **kwargs)
+
+def _repackage_jar(name, src_rule, out, rules, **kwargs):
+  """Repackages classes in a jar, to avoid collisions in the classpath."""
   repackage_tool = "@jarjar//jar"
   deploy_jar = "{src_rule}_deploy.jar".format(src_rule=src_rule)
   script_lines = []
diff --git a/clwb/src/com/google/idea/blaze/clwb/run/BlazeCidrRunConfigurationHandler.java b/clwb/src/com/google/idea/blaze/clwb/run/BlazeCidrRunConfigurationHandler.java
index 29aea18..c5ded30 100644
--- a/clwb/src/com/google/idea/blaze/clwb/run/BlazeCidrRunConfigurationHandler.java
+++ b/clwb/src/com/google/idea/blaze/clwb/run/BlazeCidrRunConfigurationHandler.java
@@ -23,6 +23,7 @@
 import com.google.idea.blaze.base.run.confighandler.BlazeCommandRunConfigurationRunner;
 import com.google.idea.blaze.base.run.state.BlazeCommandRunConfigurationCommonState;
 import com.google.idea.blaze.base.settings.Blaze;
+import com.google.idea.blaze.base.settings.Blaze.BuildSystem;
 import com.intellij.execution.Executor;
 import com.intellij.execution.RunnerAndConfigurationSettings;
 import com.intellij.execution.configurations.RunConfiguration;
@@ -38,8 +39,9 @@
   private final BlazeCommandRunConfigurationCommonState state;
 
   public BlazeCidrRunConfigurationHandler(BlazeCommandRunConfiguration configuration) {
-    this.buildSystemName = Blaze.buildSystemName(configuration.getProject());
-    this.state = new BlazeCommandRunConfigurationCommonState(buildSystemName);
+    BuildSystem buildSystem = Blaze.getBuildSystem(configuration.getProject());
+    this.buildSystemName = buildSystem.getName();
+    this.state = new BlazeCommandRunConfigurationCommonState(buildSystem);
   }
 
   @Override
diff --git a/common/actionhelper/BUILD b/common/actionhelper/BUILD
new file mode 100644
index 0000000..0da6dd5
--- /dev/null
+++ b/common/actionhelper/BUILD
@@ -0,0 +1,11 @@
+licenses(["notice"])  # Apache 2.0
+
+java_library(
+    name = "actionhelper",
+    srcs = glob(["src/**/*.java"]),
+    visibility = ["//visibility:public"],
+    deps = [
+        "//intellij_platform_sdk:plugin_api",
+        "@jsr305_annotations//jar",
+    ],
+)
diff --git a/common/actionhelper/src/com/google/idea/common/actionhelper/ActionPresentationHelper.java b/common/actionhelper/src/com/google/idea/common/actionhelper/ActionPresentationHelper.java
new file mode 100644
index 0000000..96a7882
--- /dev/null
+++ b/common/actionhelper/src/com/google/idea/common/actionhelper/ActionPresentationHelper.java
@@ -0,0 +1,175 @@
+/*
+ * 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.common.actionhelper;
+
+import com.intellij.openapi.actionSystem.AnActionEvent;
+import com.intellij.openapi.actionSystem.Presentation;
+import com.intellij.openapi.vfs.VirtualFile;
+import java.util.List;
+import javax.annotation.Nullable;
+
+/** Helps setting the presentation enabled/visible/text states. */
+public class ActionPresentationHelper {
+
+  private final Presentation presentation;
+  private boolean enabled = true;
+  private boolean visible = true;
+  private boolean disableWithoutSubject;
+  private boolean hasSubject;
+  private String text;
+  private String subjectText;
+
+  /** Converts a subject to a string */
+  @FunctionalInterface
+  public interface SubjectToString<T> {
+    String subjectToString(T subject);
+  }
+
+  private ActionPresentationHelper(Presentation presentation) {
+    this.presentation = presentation;
+  }
+
+  public static ActionPresentationHelper of(AnActionEvent e) {
+    return new ActionPresentationHelper(e.getPresentation());
+  }
+
+  /** Disables the action if the condition is true. */
+  public ActionPresentationHelper disableIf(boolean disableCondition) {
+    this.enabled = this.enabled && !disableCondition;
+    return this;
+  }
+
+  /** Hides the action if the condition is true. */
+  public ActionPresentationHelper hideIf(boolean hideCondition) {
+    this.visible = this.visible && !hideCondition;
+    return this;
+  }
+
+  /** Disables the action if no subject has been provided. */
+  public ActionPresentationHelper disableWithoutSubject() {
+    this.disableWithoutSubject = true;
+    return this;
+  }
+
+  /** Sets the text of the presentation. */
+  public ActionPresentationHelper setText(String text) {
+    this.text = text;
+    return this;
+  }
+
+  /**
+   * Sets the text depending on the subject.
+   *
+   * @param noSubjectText Text to set if there is no subject, or if the action is disabled.
+   * @param subjectText Text to set if there is a subject. If %s exists in the subject text,
+   *     String.format is used with the quoted file name.
+   * @param file The subject. May be null.
+   */
+  public ActionPresentationHelper setTextWithSubject(
+      String noSubjectText, String subjectText, @Nullable VirtualFile file) {
+    return setTextWithSubject(
+        noSubjectText, subjectText, file, ActionPresentationHelper::quoteFileName);
+  }
+
+  /**
+   * Sets the text depending on the subject.
+   *
+   * @param noSubjectText Text to set if there is no subject, or if the action is disabled.
+   * @param subjectText Text to set if there is a subject. If %s exists in the subject text,
+   *     String.format is used with the subject passed through subjectToString
+   * @param subject The subject. May be null.
+   * @param subjectToString Method used to convert the subject to a string.
+   */
+  public <T> ActionPresentationHelper setTextWithSubject(
+      String noSubjectText,
+      String subjectText,
+      @Nullable T subject,
+      SubjectToString<T> subjectToString) {
+    this.text = noSubjectText;
+    if (subject != null) {
+      this.subjectText =
+          subjectText.contains("%s")
+              ? String.format(subjectText, subjectToString.subjectToString(subject))
+              : subjectText;
+      this.hasSubject = true;
+    }
+    return this;
+  }
+
+  /**
+   * Sets the text depending on the subjects.
+   *
+   * @param noSubjectText Text to set if there is no subject, or if the action is disabled.
+   * @param singleSubjectText Text to set if there is a single subject. If %s exists in the subject
+   *     text, String.format is used with the quoted single file name.
+   * @param multipleSubjectText Text to use if there are multiple subjects.
+   */
+  public ActionPresentationHelper setTextWithSubjects(
+      String noSubjectText,
+      String singleSubjectText,
+      String multipleSubjectText,
+      List<VirtualFile> files) {
+    return setTextWithSubjects(
+        noSubjectText,
+        singleSubjectText,
+        multipleSubjectText,
+        files,
+        ActionPresentationHelper::quoteFileName);
+  }
+
+  /**
+   * Sets the text depending on the subjects.
+   *
+   * @param noSubjectText Text to set if there is no subject, or if the action is disabled.
+   * @param singleSubjectText Text to set if there is a single subject. If %s exists in the subject
+   *     text, String.format is used with the subject passed through subjectToString.
+   * @param multipleSubjectText Text to use if there are multiple subjects.
+   */
+  public <T> ActionPresentationHelper setTextWithSubjects(
+      String noSubjectText,
+      String singleSubjectText,
+      String multipleSubjectText,
+      List<T> subjects,
+      SubjectToString<T> subjectToString) {
+    if (subjects.size() > 1) {
+      this.text = noSubjectText;
+      this.subjectText = multipleSubjectText;
+      this.hasSubject = true;
+      return this;
+    } else {
+      T subject = !subjects.isEmpty() ? subjects.get(0) : null;
+      return setTextWithSubject(noSubjectText, singleSubjectText, subject, subjectToString);
+    }
+  }
+
+  private static String quoteFileName(VirtualFile file) {
+    return "\"" + file.getName() + "\"";
+  }
+
+  public void commit() {
+    boolean enabled = this.enabled;
+    if (disableWithoutSubject) {
+      enabled = enabled && hasSubject;
+    }
+    presentation.setEnabled(enabled);
+    presentation.setVisible(visible);
+
+    String text = enabled && hasSubject ? subjectText : this.text;
+    if (text != null) {
+      presentation.setText(text);
+    }
+  }
+}
diff --git a/common/formatter/BUILD b/common/formatter/BUILD
index 839a469..1e45774 100644
--- a/common/formatter/BUILD
+++ b/common/formatter/BUILD
@@ -6,6 +6,7 @@
     visibility = ["//visibility:public"],
     deps = [
         "//intellij_platform_sdk:plugin_api",
+        "//sdkcompat",
         "@jsr305_annotations//jar",
     ],
 )
diff --git a/common/formatter/src/com/google/idea/common/formatter/DelegatingCodeStyleManager.java b/common/formatter/src/com/google/idea/common/formatter/DelegatingCodeStyleManager.java
index aa2460f..354fea4 100644
--- a/common/formatter/src/com/google/idea/common/formatter/DelegatingCodeStyleManager.java
+++ b/common/formatter/src/com/google/idea/common/formatter/DelegatingCodeStyleManager.java
@@ -15,6 +15,7 @@
  */
 package com.google.idea.common.formatter;
 
+import com.google.idea.sdkcompat.codestyle.CodeStyleManagerSdkCompatAdapter;
 import com.intellij.lang.ASTNode;
 import com.intellij.openapi.editor.Document;
 import com.intellij.openapi.fileTypes.FileType;
@@ -31,7 +32,7 @@
 import javax.annotation.Nullable;
 
 /** A delegating {@link CodeStyleManager}. */
-public abstract class DelegatingCodeStyleManager extends CodeStyleManager {
+public abstract class DelegatingCodeStyleManager extends CodeStyleManagerSdkCompatAdapter {
 
   private final CodeStyleManager delegate;
 
diff --git a/cpp/src/com/google/idea/blaze/cpp/BlazeCSyncPlugin.java b/cpp/src/com/google/idea/blaze/cpp/BlazeCSyncPlugin.java
index c066fc4..1c64fbc 100644
--- a/cpp/src/com/google/idea/blaze/cpp/BlazeCSyncPlugin.java
+++ b/cpp/src/com/google/idea/blaze/cpp/BlazeCSyncPlugin.java
@@ -27,11 +27,9 @@
 import com.google.idea.blaze.base.sync.BlazeSyncPlugin;
 import com.intellij.openapi.module.Module;
 import com.intellij.openapi.project.Project;
-import com.intellij.openapi.roots.ModifiableRootModel;
 import com.jetbrains.cidr.lang.workspace.OCWorkspace;
 import com.jetbrains.cidr.lang.workspace.OCWorkspaceManager;
 import java.util.Set;
-import javax.annotation.Nullable;
 
 final class BlazeCSyncPlugin extends BlazeSyncPlugin.Adapter {
   @Override
@@ -43,16 +41,13 @@
   }
 
   @Override
-  public void updateProjectStructure(
+  public void updateInMemoryState(
       Project project,
       BlazeContext context,
       WorkspaceRoot workspaceRoot,
       ProjectViewSet projectViewSet,
       BlazeProjectData blazeProjectData,
-      @Nullable BlazeProjectData oldBlazeProjectData,
-      ModuleEditor moduleEditor,
-      Module workspaceModule,
-      ModifiableRootModel workspaceModifiableModel) {
+      Module workspaceModule) {
     if (!blazeProjectData.workspaceLanguageSettings.isLanguageActive(LanguageClass.C)) {
       return;
     }
diff --git a/golang/BUILD b/golang/BUILD
new file mode 100644
index 0000000..16a486c
--- /dev/null
+++ b/golang/BUILD
@@ -0,0 +1,38 @@
+licenses(["notice"])  # Apache 2.0
+
+java_library(
+    name = "golang",
+    srcs = glob(["src/**/*.java"]),
+    visibility = ["//visibility:public"],
+    deps = [
+        "//base",
+        "//intellij_platform_sdk:plugin_api",
+        "//sdkcompat",
+        "@jsr305_annotations//jar",
+    ],
+)
+
+filegroup(
+    name = "plugin_xml",
+    srcs = ["src/META-INF/golang.xml"],
+    visibility = ["//visibility:public"],
+)
+
+load(
+    "//testing:test_defs.bzl",
+    "intellij_unit_test_suite",
+)
+
+intellij_unit_test_suite(
+    name = "unit_tests",
+    srcs = glob(["tests/unittests/**/*.java"]),
+    test_package_root = "com.google.idea.blaze.golang",
+    deps = [
+        ":golang",
+        "//base",
+        "//base:unit_test_utils",
+        "//intellij_platform_sdk:plugin_api_for_tests",
+        "@jsr305_annotations//jar",
+        "@junit//jar",
+    ],
+)
diff --git a/golang/src/META-INF/golang.xml b/golang/src/META-INF/golang.xml
new file mode 100644
index 0000000..32948ee
--- /dev/null
+++ b/golang/src/META-INF/golang.xml
@@ -0,0 +1,7 @@
+<idea-plugin>
+
+  <extensions defaultExtensionNs="com.google.idea.blaze">
+    <SyncPlugin implementation="com.google.idea.blaze.golang.sync.BlazeGoSyncPlugin"/>
+  </extensions>
+
+</idea-plugin>
diff --git a/golang/src/com/google/idea/blaze/golang/sdk/GoSdkUtil.java b/golang/src/com/google/idea/blaze/golang/sdk/GoSdkUtil.java
new file mode 100644
index 0000000..ba42571
--- /dev/null
+++ b/golang/src/com/google/idea/blaze/golang/sdk/GoSdkUtil.java
@@ -0,0 +1,59 @@
+/*
+ * 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.golang.sdk;
+
+import com.intellij.execution.configurations.PathEnvironmentVariableUtil;
+import com.intellij.openapi.util.text.StringUtil;
+import com.intellij.openapi.vfs.LocalFileSystem;
+import com.intellij.openapi.vfs.VirtualFile;
+import java.io.File;
+import java.io.IOException;
+import javax.annotation.Nullable;
+
+/**
+ * Go-lang SDK utility methods.
+ *
+ * <p>TODO: Remove this, and reference go-lang plugin source code directly.
+ */
+public class GoSdkUtil {
+
+  @Nullable
+  public static VirtualFile suggestSdkDirectory() {
+    String fromEnv = suggestSdkDirectoryPathFromEnv();
+    if (fromEnv != null) {
+      return LocalFileSystem.getInstance().findFileByPath(fromEnv);
+    }
+    return LocalFileSystem.getInstance().findFileByPath("/usr/local/go");
+  }
+
+  @Nullable
+  private static String suggestSdkDirectoryPathFromEnv() {
+    File fileFromPath = PathEnvironmentVariableUtil.findInPath("go");
+    if (fileFromPath != null) {
+      File canonicalFile;
+      try {
+        canonicalFile = fileFromPath.getCanonicalFile();
+        String path = canonicalFile.getPath();
+        if (path.endsWith("bin/go")) {
+          return StringUtil.trimEnd(path, "bin/go");
+        }
+      } catch (IOException e) {
+        // if it can't be found, just silently return null
+      }
+    }
+    return null;
+  }
+}
diff --git a/golang/src/com/google/idea/blaze/golang/sync/BlazeGoLibrarySource.java b/golang/src/com/google/idea/blaze/golang/sync/BlazeGoLibrarySource.java
new file mode 100644
index 0000000..693c5f7
--- /dev/null
+++ b/golang/src/com/google/idea/blaze/golang/sync/BlazeGoLibrarySource.java
@@ -0,0 +1,38 @@
+/*
+ * 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.golang.sync;
+
+import com.google.idea.blaze.base.sync.libraries.LibrarySource;
+import com.intellij.openapi.roots.libraries.Library;
+import java.util.function.Predicate;
+
+/** Prevents garbage collection of Go libraries */
+class BlazeGoLibrarySource extends LibrarySource.Adapter {
+
+  static final BlazeGoLibrarySource INSTANCE = new BlazeGoLibrarySource();
+
+  private BlazeGoLibrarySource() {}
+
+  @Override
+  public Predicate<Library> getGcRetentionFilter() {
+    return BlazeGoLibrarySource::isGoLibrary;
+  }
+
+  static boolean isGoLibrary(Library library) {
+    String name = library.getName();
+    return name != null && name.startsWith(BlazeGoSyncPlugin.GO_LIBRARY_PREFIX);
+  }
+}
diff --git a/golang/src/com/google/idea/blaze/golang/sync/BlazeGoSyncPlugin.java b/golang/src/com/google/idea/blaze/golang/sync/BlazeGoSyncPlugin.java
new file mode 100644
index 0000000..7f2a5c7
--- /dev/null
+++ b/golang/src/com/google/idea/blaze/golang/sync/BlazeGoSyncPlugin.java
@@ -0,0 +1,243 @@
+/*
+ * 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.golang.sync;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.Lists;
+import com.google.idea.blaze.base.model.BlazeProjectData;
+import com.google.idea.blaze.base.model.BlazeVersionData;
+import com.google.idea.blaze.base.model.primitives.LanguageClass;
+import com.google.idea.blaze.base.model.primitives.WorkspaceRoot;
+import com.google.idea.blaze.base.model.primitives.WorkspaceType;
+import com.google.idea.blaze.base.projectview.ProjectViewSet;
+import com.google.idea.blaze.base.scope.BlazeContext;
+import com.google.idea.blaze.base.scope.output.IssueOutput;
+import com.google.idea.blaze.base.sync.BlazeSyncPlugin;
+import com.google.idea.blaze.base.sync.GenericSourceFolderProvider;
+import com.google.idea.blaze.base.sync.SourceFolderProvider;
+import com.google.idea.blaze.base.sync.data.BlazeDataStorage;
+import com.google.idea.blaze.base.sync.libraries.LibrarySource;
+import com.google.idea.blaze.base.sync.projectview.WorkspaceLanguageSettings;
+import com.google.idea.blaze.golang.sdk.GoSdkUtil;
+import com.google.idea.sdkcompat.transactions.Transactions;
+import com.intellij.ide.plugins.IdeaPluginDescriptor;
+import com.intellij.ide.plugins.PluginManager;
+import com.intellij.openapi.application.ApplicationManager;
+import com.intellij.openapi.extensions.PluginId;
+import com.intellij.openapi.module.Module;
+import com.intellij.openapi.module.ModuleType;
+import com.intellij.openapi.module.ModuleTypeManager;
+import com.intellij.openapi.project.Project;
+import com.intellij.openapi.projectRoots.ProjectJdkTable;
+import com.intellij.openapi.projectRoots.Sdk;
+import com.intellij.openapi.projectRoots.SdkType;
+import com.intellij.openapi.projectRoots.SdkTypeId;
+import com.intellij.openapi.projectRoots.impl.SdkConfigurationUtil;
+import com.intellij.openapi.roots.ModifiableRootModel;
+import com.intellij.openapi.roots.ProjectRootManager;
+import com.intellij.openapi.roots.libraries.Library;
+import com.intellij.openapi.roots.libraries.LibraryTablesRegistrar;
+import com.intellij.openapi.updateSettings.impl.pluginsAdvertisement.PluginsAdvertiser;
+import com.intellij.openapi.util.EmptyRunnable;
+import com.intellij.openapi.vfs.VirtualFile;
+import com.intellij.pom.NavigatableAdapter;
+import java.util.List;
+import java.util.Set;
+import javax.annotation.Nullable;
+
+/** Supports golang. */
+public class BlazeGoSyncPlugin extends BlazeSyncPlugin.Adapter {
+
+  static final String GO_LIBRARY_PREFIX = "GOPATH";
+  private static final String GO_MODULE_TYPE_ID = "GO_MODULE";
+  private static final String GO_PLUGIN_ID = "ro.redeul.google.go";
+  private static final String GO_SDK_TYPE_ID = "Go SDK";
+
+  @Nullable
+  @Override
+  public ModuleType<?> getWorkspaceModuleType(WorkspaceType workspaceType) {
+    if (workspaceType == WorkspaceType.GO) {
+      return ModuleTypeManager.getInstance().findByID(GO_MODULE_TYPE_ID);
+    }
+    return null;
+  }
+
+  @Override
+  public ImmutableList<WorkspaceType> getSupportedWorkspaceTypes() {
+    return ImmutableList.of(WorkspaceType.GO);
+  }
+
+  @Override
+  public Set<LanguageClass> getSupportedLanguagesInWorkspace(WorkspaceType workspaceType) {
+    if (workspaceType == WorkspaceType.GO) {
+      return ImmutableSet.of(LanguageClass.GO);
+    }
+    return ImmutableSet.of();
+  }
+
+  @Nullable
+  @Override
+  public WorkspaceType getDefaultWorkspaceType() {
+    return WorkspaceType.GO;
+  }
+
+  @Nullable
+  @Override
+  public SourceFolderProvider getSourceFolderProvider(BlazeProjectData projectData) {
+    if (!projectData.workspaceLanguageSettings.isWorkspaceType(WorkspaceType.GO)) {
+      return null;
+    }
+    return GenericSourceFolderProvider.INSTANCE;
+  }
+
+  @Override
+  public void updateProjectStructure(
+      Project project,
+      BlazeContext context,
+      WorkspaceRoot workspaceRoot,
+      ProjectViewSet projectViewSet,
+      BlazeProjectData blazeProjectData,
+      @Nullable BlazeProjectData oldBlazeProjectData,
+      ModuleEditor moduleEditor,
+      Module workspaceModule,
+      ModifiableRootModel workspaceModifiableModel) {
+    if (!blazeProjectData.workspaceLanguageSettings.isLanguageActive(LanguageClass.GO)) {
+      return;
+    }
+    for (Library lib : getGoLibraries(project)) {
+      if (workspaceModifiableModel.findLibraryOrderEntry(lib) == null) {
+        workspaceModifiableModel.addLibraryEntry(lib);
+      }
+    }
+  }
+
+  private static List<Library> getGoLibraries(Project project) {
+    List<Library> libraries = Lists.newArrayList();
+    LibraryTablesRegistrar registrar = LibraryTablesRegistrar.getInstance();
+    for (Library lib : registrar.getLibraryTable().getLibraries()) {
+      if (BlazeGoLibrarySource.isGoLibrary(lib)) {
+        libraries.add(lib);
+      }
+    }
+
+    String moduleLibraryName =
+        String.format("%s <%s>", GO_LIBRARY_PREFIX, BlazeDataStorage.WORKSPACE_MODULE_NAME);
+    Library goModuleLibrary =
+        registrar.getLibraryTable(project).getLibraryByName(moduleLibraryName);
+    if (goModuleLibrary != null) {
+      libraries.add(goModuleLibrary);
+    }
+    return libraries;
+  }
+
+  /**
+   * By default the Go plugin will create duplicate copies of project libraries, one for each
+   * module. We only care about library associated with the workspace module.
+   */
+  static boolean isGoLibraryForModule(Library library, String moduleName) {
+    String name = library.getName();
+    return name != null && name.equals("GOPATH <" + moduleName + ">");
+  }
+
+  @Nullable
+  @Override
+  public LibrarySource getLibrarySource(BlazeProjectData blazeProjectData) {
+    if (!blazeProjectData.workspaceLanguageSettings.isLanguageActive(LanguageClass.GO)) {
+      return null;
+    }
+    return BlazeGoLibrarySource.INSTANCE;
+  }
+
+  @Override
+  public boolean validateProjectView(
+      BlazeContext context,
+      ProjectViewSet projectViewSet,
+      WorkspaceLanguageSettings workspaceLanguageSettings) {
+    if (!workspaceLanguageSettings.isLanguageActive(LanguageClass.GO)) {
+      return true;
+    }
+    if (!isPluginEnabled()) {
+      IssueOutput.error("Go plugin needed for Go language support.")
+          .navigatable(
+              new NavigatableAdapter() {
+                @Override
+                public void navigate(boolean requestFocus) {
+                  if (isPluginInstalled()) {
+                    PluginManager.enablePlugin(GO_PLUGIN_ID);
+                  } else {
+                    PluginsAdvertiser.installAndEnablePlugins(
+                        ImmutableSet.of(GO_PLUGIN_ID), EmptyRunnable.INSTANCE);
+                  }
+                }
+              })
+          .submit(context);
+      return false;
+    }
+    return true;
+  }
+
+  private static boolean isPluginInstalled() {
+    return PluginManager.isPluginInstalled(PluginId.getId(GO_PLUGIN_ID));
+  }
+
+  private static boolean isPluginEnabled() {
+    IdeaPluginDescriptor plugin = PluginManager.getPlugin(PluginId.getId(GO_PLUGIN_ID));
+    return plugin != null && plugin.isEnabled();
+  }
+
+  @Override
+  public void updateProjectSdk(
+      Project project,
+      BlazeContext context,
+      ProjectViewSet projectViewSet,
+      BlazeVersionData blazeVersionData,
+      BlazeProjectData blazeProjectData) {
+    if (!blazeProjectData.workspaceLanguageSettings.isWorkspaceType(WorkspaceType.GO)) {
+      return;
+    }
+    Sdk currentSdk = ProjectRootManager.getInstance(project).getProjectSdk();
+    if (currentSdk != null && currentSdk.getSdkType().getName().equals(GO_SDK_TYPE_ID)) {
+      return;
+    }
+    Sdk sdk = getOrCreateGoSdk();
+    if (sdk != null) {
+      setProjectSdk(project, sdk);
+    }
+  }
+
+  @Nullable
+  private static Sdk getOrCreateGoSdk() {
+    ProjectJdkTable sdkTable = ProjectJdkTable.getInstance();
+    SdkTypeId type = sdkTable.getSdkTypeByName(GO_SDK_TYPE_ID);
+    List<Sdk> sdk = sdkTable.getSdksOfType(type);
+    if (!sdk.isEmpty()) {
+      return sdk.get(0);
+    }
+    VirtualFile defaultSdk = GoSdkUtil.suggestSdkDirectory();
+    if (defaultSdk != null) {
+      return SdkConfigurationUtil.createAndAddSDK(defaultSdk.getPath(), (SdkType) type);
+    }
+    return null;
+  }
+
+  private static void setProjectSdk(Project project, Sdk sdk) {
+    Transactions.submitTransactionAndWait(
+        () ->
+            ApplicationManager.getApplication()
+                .runWriteAction(() -> ProjectRootManager.getInstance(project).setProjectSdk(sdk)));
+  }
+}
diff --git a/golang/tests/unittests/com/google/idea/blaze/golang/sync/BlazeGoSyncPluginTest.java b/golang/tests/unittests/com/google/idea/blaze/golang/sync/BlazeGoSyncPluginTest.java
new file mode 100644
index 0000000..e563a27
--- /dev/null
+++ b/golang/tests/unittests/com/google/idea/blaze/golang/sync/BlazeGoSyncPluginTest.java
@@ -0,0 +1,116 @@
+/*
+ * 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.golang.sync;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableSet;
+import com.google.idea.blaze.base.BlazeTestCase;
+import com.google.idea.blaze.base.model.primitives.LanguageClass;
+import com.google.idea.blaze.base.model.primitives.WorkspaceType;
+import com.google.idea.blaze.base.projectview.ProjectView;
+import com.google.idea.blaze.base.projectview.ProjectViewSet;
+import com.google.idea.blaze.base.projectview.section.ListSection;
+import com.google.idea.blaze.base.projectview.section.ScalarSection;
+import com.google.idea.blaze.base.projectview.section.sections.AdditionalLanguagesSection;
+import com.google.idea.blaze.base.projectview.section.sections.WorkspaceTypeSection;
+import com.google.idea.blaze.base.scope.BlazeContext;
+import com.google.idea.blaze.base.scope.ErrorCollector;
+import com.google.idea.blaze.base.scope.output.IssueOutput;
+import com.google.idea.blaze.base.sync.BlazeSyncPlugin;
+import com.google.idea.blaze.base.sync.projectview.LanguageSupport;
+import com.google.idea.blaze.base.sync.projectview.WorkspaceLanguageSettings;
+import com.intellij.openapi.extensions.impl.ExtensionPointImpl;
+import java.util.Set;
+import javax.annotation.Nullable;
+import org.jetbrains.annotations.NotNull;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** Unit tests for {@link BlazeGoSyncPlugin} */
+@RunWith(JUnit4.class)
+public class BlazeGoSyncPluginTest extends BlazeTestCase {
+
+  private final ErrorCollector errorCollector = new ErrorCollector();
+  private BlazeContext context;
+  private ExtensionPointImpl<BlazeSyncPlugin> syncPluginEp;
+
+  @Override
+  protected void initTest(
+      @NotNull Container applicationServices, @NotNull Container projectServices) {
+    super.initTest(applicationServices, projectServices);
+
+    syncPluginEp = registerExtensionPoint(BlazeSyncPlugin.EP_NAME, BlazeSyncPlugin.class);
+    syncPluginEp.registerExtension(new BlazeGoSyncPlugin());
+    context = new BlazeContext();
+    context.addOutputSink(IssueOutput.class, errorCollector);
+  }
+
+  @Test
+  public void testGoWorkspaceTypeSupported() {
+    ProjectViewSet projectViewSet =
+        ProjectViewSet.builder()
+            .add(
+                ProjectView.builder()
+                    .add(ScalarSection.builder(WorkspaceTypeSection.KEY).set(WorkspaceType.GO))
+                    .build())
+            .build();
+    WorkspaceLanguageSettings workspaceLanguageSettings =
+        LanguageSupport.createWorkspaceLanguageSettings(context, projectViewSet);
+    errorCollector.assertNoIssues();
+    assertThat(workspaceLanguageSettings)
+        .isEqualTo(
+            new WorkspaceLanguageSettings(
+                WorkspaceType.GO, ImmutableSet.of(LanguageClass.GENERIC, LanguageClass.GO)));
+  }
+
+  @Test
+  public void testGoNotAValidAdditionalLanguage() {
+    // add a java sync plugin so we have another workspace type available
+    syncPluginEp.registerExtension(
+        new BlazeSyncPlugin.Adapter() {
+          @Override
+          public ImmutableList<WorkspaceType> getSupportedWorkspaceTypes() {
+            return ImmutableList.of(WorkspaceType.JAVA);
+          }
+
+          @Override
+          public Set<LanguageClass> getSupportedLanguagesInWorkspace(WorkspaceType workspaceType) {
+            return ImmutableSet.of(LanguageClass.JAVA);
+          }
+
+          @Nullable
+          @Override
+          public WorkspaceType getDefaultWorkspaceType() {
+            return WorkspaceType.JAVA;
+          }
+        });
+
+    ProjectViewSet projectViewSet =
+        ProjectViewSet.builder()
+            .add(
+                ProjectView.builder()
+                    .add(ScalarSection.builder(WorkspaceTypeSection.KEY).set(WorkspaceType.JAVA))
+                    .add(ListSection.builder(AdditionalLanguagesSection.KEY).add(LanguageClass.GO))
+                    .build())
+            .build();
+    LanguageSupport.createWorkspaceLanguageSettings(context, projectViewSet);
+    errorCollector.assertIssueContaining(
+        "Language 'go' is not supported for this plugin with workspace type: 'java'");
+  }
+}
diff --git a/ijwb/BUILD b/ijwb/BUILD
index 0ec5aad..55b2b72 100644
--- a/ijwb/BUILD
+++ b/ijwb/BUILD
@@ -17,6 +17,7 @@
     srcs = [
         "src/META-INF/ijwb.xml",
         "//base:plugin_xml",
+        "//golang:plugin_xml",
         "//java:plugin_xml",
         "//plugin_dev:plugin_xml",
     ],
@@ -50,6 +51,7 @@
     ],
     deps = [
         "//base",
+        "//golang",
         "//intellij_platform_sdk:plugin_api",
         "//java",
         "@jsr305_annotations//jar",
diff --git a/ijwb/ijwb.bazelproject b/ijwb/ijwb.bazelproject
index e8fc755..6bf0977 100644
--- a/ijwb/ijwb.bazelproject
+++ b/ijwb/ijwb.bazelproject
@@ -11,6 +11,9 @@
 
 workspace_type: intellij_plugin
 
+build_flags:
+  --define=ij_product=intellij-latest
+
 test_sources:
   */tests/unittests*
   */tests/integrationtests*
diff --git a/intellij_platform_sdk/BUILD b/intellij_platform_sdk/BUILD
index 2f68436..9edcbbf 100644
--- a/intellij_platform_sdk/BUILD
+++ b/intellij_platform_sdk/BUILD
@@ -62,6 +62,14 @@
     },
 )
 
+# Android Studio 2.3.0.4
+config_setting(
+    name = "android-studio-2.3.0.4",
+    values = {
+        "define": "ij_product=android-studio-2.3.0.4",
+    },
+)
+
 config_setting(
     name = "clion-latest",
     values = {
@@ -69,6 +77,14 @@
     },
 )
 
+# CLion 2016.3.2
+config_setting(
+    name = "clion-2016.3.2",
+    values = {
+        "define": "ij_product=clion-2016.3.2",
+    },
+)
+
 # CLion 2016.2.2
 config_setting(
     name = "clion-162.1967.7",
@@ -77,16 +93,13 @@
     },
 )
 
-# CLion 16 (2016.2.1)
-config_setting(
-    name = "clion-162.1628.20",
-    values = {
-        "define": "ij_product=clion-162.1628.20",
-    },
+load(
+    ":build_defs.bzl",
+    "select_for_ide",
+    "select_for_plugin_api",
+    "select_from_plugin_api_directory",
 )
 
-load(":build_defs.bzl", "select_from_plugin_api_directory", "select_for_ide")
-
 # The purpose of this rule is to hide the versioning
 # complexity from users of this api.
 # There will be additional versions added in the future
@@ -96,6 +109,7 @@
         android_studio = [
             ":sdk",
             ":android_plugin",
+            ":test_recorder",
         ],
         clion = [":sdk"],
         intellij = [":sdk"],
@@ -128,7 +142,7 @@
 java_library(
     name = "plugin_api_for_grammar_kit",
     visibility = ["//third_party/java/jetbrains/grammar_kit:__pkg__"],
-    exports = ["//intellij_platform_sdk/IC_162_2032_8:sdk"],
+    exports = ["//intellij_platform_sdk/intellij_ce_2016_3_1:sdk"],
 )
 
 # Used to support IntelliJ plugin development in our plugin
@@ -172,7 +186,25 @@
         android_studio = [":bundled_plugins"],
         clion = [":bundled_plugins"],
         intellij = [":bundled_plugins"],
-    ),
+    ) + [":missing_test_classes"],
+)
+
+# Certain plugin API releases don't contain test classes, so any testServiceImplemention
+# in an upstream plugin will cause an error when the plugin is loaded for integration tests.
+# Here we have dummy versions of these missing classes.
+java_library(
+    name = "missing_test_classes",
+    srcs = select_for_plugin_api({
+        "android-studio-2.3.0.3": [
+            "missing/tests/com/jetbrains/cidr/modulemap/resolve/MockModuleMapManagerImpl.java",
+        ],
+        "android-studio-2.3.0.4": [
+            "missing/tests/com/jetbrains/cidr/modulemap/resolve/MockModuleMapManagerImpl.java",
+        ],
+        "default": [],
+    }) + ["missing/src/dummy/pkg/DummyClassToAvoidAnEmptyJavaLibrary.java"],
+    tags = ["intellij-missing-test-classes"],
+    deps = [":plugin_api"],
 )
 
 filegroup(
@@ -189,6 +221,7 @@
     srcs = select_for_ide(
         android_studio = ["android_studio_application_info_name.txt"],
         clion = ["clion_application_info_name.txt"],
+        default = ["intellij_application_info_name.txt"],
         intellij = ["intellij_application_info_name.txt"],
     ),
 )
diff --git a/intellij_platform_sdk/BUILD.clion b/intellij_platform_sdk/BUILD.clion
index e395d09..c83e150 100644
--- a/intellij_platform_sdk/BUILD.clion
+++ b/intellij_platform_sdk/BUILD.clion
@@ -26,4 +26,4 @@
 filegroup(
     name = "application_info_jar",
     srcs = glob(["clion-*/lib/clion.jar"]),
-)
+)
\ No newline at end of file
diff --git a/intellij_platform_sdk/build_defs.bzl b/intellij_platform_sdk/build_defs.bzl
index 57dbe76..e7c5c2d 100644
--- a/intellij_platform_sdk/build_defs.bzl
+++ b/intellij_platform_sdk/build_defs.bzl
@@ -2,7 +2,7 @@
 
 # The current indirect ij_product mapping (eg. "intellij-latest")
 INDIRECT_IJ_PRODUCTS = {
-    "intellij-latest": "intellij-162.2032.8",
+    "intellij-latest": "intellij-2016.3.1",
     "android-studio-latest": "android-studio-145.1617.8",
     "android-studio-beta": "android-studio-2.3.0.3",
     "clion-latest": "clion-162.1967.7",
@@ -25,14 +25,18 @@
         ide="android-studio",
         directory="android_studio_2_3_0_3",
     ),
-    "clion-162.1628.20": struct(
-        ide="clion",
-        directory="CL_162_1628_20",
+    "android-studio-2.3.0.4": struct(
+        ide="android-studio",
+        directory="android_studio_2_3_0_4",
     ),
     "clion-162.1967.7": struct(
         ide="clion",
         directory="CL_162_1967_7",
     ),
+    "clion-2016.3.2": struct(
+        ide="clion",
+        directory="clion_2016_3_2",
+    ),
 }
 
 # BUILD_VARS for each IDE corresponding to indirect ij_products, eg. "intellij-latest"
@@ -50,11 +54,8 @@
               You may only include direct ij_products here,
               not indirects (eg. intellij-latest).
   Returns:
-      A select statement on all plugin_apis. Unless you include a "default"
-      clause any other matched plugin_api will return "None".
-
-      A build without an ij_product is considered equivalent to building with
-      "intellij-latest".
+      A select statement on all plugin_apis. Unless you include a "default",
+      a non-matched plugin_api will result in an error.
 
   Example:
     java_library(
@@ -65,6 +66,9 @@
       }),
     )
   """
+  if not params:
+    fail("Empty select_for_plugin_api")
+
   for indirect_ij_product in INDIRECT_IJ_PRODUCTS:
     if indirect_ij_product in params:
       error_message = "".join([
@@ -72,43 +76,27 @@
           "Instead, select on an exact ij_product."])
       fail(error_message)
 
-  # To make the select work with "intellij-latest" and friends,
-  # we find if the user is currently selecting on what intellij-latest
-  # is resolving to, and copy that. Example:
+  expanded_params = dict(**params)
+
+  # Expand all indirect plugin_apis to point to their
+  # corresponding direct plugin_api.
   #
-  # {"intellij-2016.3.1": "stuff"} ->
-  # {"intellij-2016.3.1": "stuff", "intellij-latest": "stuff"}
-  params = dict(**params)
+  # {"intellij-2016.3.1": "foo"} ->
+  # {"intellij-2016.3.1": "foo", "intellij-latest": "foo"}
   for indirect_ij_product, resolved_plugin_api in INDIRECT_IJ_PRODUCTS.items():
     if resolved_plugin_api in params:
-      params[indirect_ij_product] = params[resolved_plugin_api]
+      expanded_params[indirect_ij_product] = params[resolved_plugin_api]
 
-  if "default" not in params:
-    # If "intellij-latest" is supported, we set "default" to that
-    # This supports building with an empty command line. Example:
-    #
-    # {"intellij-2016.3.1": "stuff", "intellij-latest": "stuff"} ->
-    # {"intellij-2016.3.1": "stuff", "intellij-latest": "stuff", "default": "stuff"}
-    if "intellij-latest" in params:
-      params["default"] = params["intellij-latest"]
-
-    # Add the other indirect ij_products returning None rather than default
-    for ij_product in INDIRECT_IJ_PRODUCTS:
-      if ij_product not in params:
-        params[ij_product] = None
-    for ij_product in DIRECT_IJ_PRODUCTS:
-      if ij_product not in params:
-        params[ij_product] = None
-
-  # Map to the actual targets
+  # Map the shorthand ij_products to full config_setting targets.
   # This makes it more convenient so the user doesn't have to
   # fully specify the path to the plugin_apis
   select_params = dict()
-  for ij_product, value in params.items():
+  for ij_product, value in expanded_params.items():
     if ij_product == "default":
       select_params["//conditions:default"] = value
     else:
       select_params["//intellij_platform_sdk:" + ij_product] = value
+
   return select(select_params)
 
 def select_for_ide(intellij=None, android_studio=None, clion=None, default=None):
@@ -134,7 +122,6 @@
   intellij = intellij or default
   android_studio = android_studio or default
   clion = clion or default
-  default = default or intellij
 
   ide_to_value = {
       "intellij" : intellij,
@@ -166,4 +153,8 @@
   params = dict()
   for ij_product, value in DIRECT_IJ_PRODUCTS.items():
     params[ij_product] = [_plugin_api_directory(value) + item for item in ide_to_value[value.ide]]
+
+  # No ij_product == intellij-latest
+  params["default"] = params[INDIRECT_IJ_PRODUCTS["intellij-latest"]]
+
   return select_for_plugin_api(params)
diff --git a/java/BUILD b/java/BUILD
index 80799c3..ac9f772 100644
--- a/java/BUILD
+++ b/java/BUILD
@@ -6,10 +6,12 @@
     visibility = ["//visibility:public"],
     deps = [
         "//base",
+        "//common/actionhelper",
         "//common/experiments",
         "//intellij_platform_sdk:junit",
         "//intellij_platform_sdk:plugin_api",
         "//proto_deps",
+        "//sdkcompat",
         "@jsr305_annotations//jar",
     ],
 )
diff --git a/java/src/META-INF/blaze-java.xml b/java/src/META-INF/blaze-java.xml
index ae7ca4d..5466c27 100644
--- a/java/src/META-INF/blaze-java.xml
+++ b/java/src/META-INF/blaze-java.xml
@@ -19,31 +19,34 @@
 
   <actions>
     <action class="com.google.idea.blaze.java.libraries.ExcludeLibraryAction"
-            id="Blaze.ExcludeLibraryAction"
-            icon="BlazeIcons.Blaze"
-            text="Exclude Library and Resync">
-      <add-to-group group-id="Blaze.ProjectViewPopupMenu"/>
+      id="Blaze.ExcludeLibraryAction"
+      text="Exclude Library and Resync">
     </action>
     <action class="com.google.idea.blaze.java.libraries.AttachSourceJarAction"
-            id="Blaze.AttachSourceJarAction"
-            icon="BlazeIcons.Blaze"
-            text="Attach Source Jar">
-      <add-to-group group-id="Blaze.ProjectViewPopupMenu"/>
+      id="Blaze.AttachSourceJarAction"
+      text="Attach Source Jar">
     </action>
     <action class="com.google.idea.blaze.java.libraries.AddLibraryTargetDirectoryToProjectViewAction"
-            id="Blaze.AddLibraryTargetDirectoryToProjectView"
-            icon="BlazeIcons.Blaze"
-            text="Add Library Target Directory To Project View">
-      <add-to-group group-id="Blaze.ProjectViewPopupMenu"/>
+      id="Blaze.AddLibraryTargetDirectoryToProjectView"
+      text="Add Library Target Directory to Project View">
+    </action>
+    <action class="com.google.idea.blaze.java.libraries.DetachAllSourceJarsAction"
+      id="Blaze.DetachAllSourceJars"
+      text="Detach All Blaze Source Jars">
     </action>
 
-    <group>
-      <action class="com.google.idea.blaze.java.libraries.DetachAllSourceJarsAction"
-        id="Blaze.DetachAllSourceJars"
-        text="Detach All Blaze Source Jars">
-      </action>
-      <separator/>
-      <add-to-group group-id="Blaze.MainMenuActionGroup" relative-to-action="Blaze.EditProjectView" anchor="before"/>
+    <group id="Blaze.Java.ProjectViewPopupMenu">
+      <add-to-group group-id="Blaze.ProjectViewPopupMenu"/>
+      <reference id="Blaze.ExcludeLibraryAction"/>
+      <reference id="Blaze.AttachSourceJarAction"/>
+      <reference id="Blaze.AddLibraryTargetDirectoryToProjectView"/>
+    </group>
+
+    <group id="Blaze.JavaMenuGroup.Outer">
+      <add-to-group group-id="Blaze.MainMenuActionGroup" relative-to-action="Blaze.MenuFooter" anchor="after"/>
+      <group id="Blaze.JavaMenuGroup" text="Java">
+        <reference id="Blaze.DetachAllSourceJars"/>
+      </group>
     </group>
 
     <!-- IntelliJ specific actions -->
@@ -92,6 +95,9 @@
     <projectService serviceImplementation="com.google.idea.blaze.java.libraries.SourceJarManager"/>
     <refactoring.safeDeleteProcessor id="build_file_safe_delete" order="before javaProcessor"
                                      implementation="com.google.idea.blaze.java.lang.build.BuildFileSafeDeleteProcessor"/>
+    <!--duplicated here in case the Kotlin plugin is present, as it also tries to replace javaProcessor-->
+    <refactoring.safeDeleteProcessor id="build_file_safe_delete_copy" order="before kotlinProcessor"
+                                     implementation="com.google.idea.blaze.java.lang.build.BuildFileSafeDeleteProcessor"/>
     <projectService serviceImplementation="com.google.idea.blaze.java.libraries.JarCache"/>
 
     <attachSourcesProvider implementation="com.google.idea.blaze.java.libraries.AddLibraryTargetDirectoryToProjectViewAttachSourcesProvider"/>
diff --git a/java/src/com/google/idea/blaze/java/libraries/AddLibraryTargetDirectoryToProjectViewAction.java b/java/src/com/google/idea/blaze/java/libraries/AddLibraryTargetDirectoryToProjectViewAction.java
index 935fded..a954942 100644
--- a/java/src/com/google/idea/blaze/java/libraries/AddLibraryTargetDirectoryToProjectViewAction.java
+++ b/java/src/com/google/idea/blaze/java/libraries/AddLibraryTargetDirectoryToProjectViewAction.java
@@ -18,7 +18,7 @@
 import com.google.common.base.Strings;
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.Sets;
-import com.google.idea.blaze.base.actions.BlazeAction;
+import com.google.idea.blaze.base.actions.BlazeProjectAction;
 import com.google.idea.blaze.base.ideinfo.TargetIdeInfo;
 import com.google.idea.blaze.base.ideinfo.TargetKey;
 import com.google.idea.blaze.base.model.BlazeProjectData;
@@ -44,14 +44,12 @@
 import java.io.File;
 import java.util.List;
 import java.util.Set;
-import org.jetbrains.annotations.NotNull;
-import org.jetbrains.annotations.Nullable;
+import javax.annotation.Nullable;
 
-class AddLibraryTargetDirectoryToProjectViewAction extends BlazeAction {
+class AddLibraryTargetDirectoryToProjectViewAction extends BlazeProjectAction {
+
   @Override
-  public void actionPerformed(AnActionEvent e) {
-    Project project = e.getProject();
-    assert project != null;
+  protected void actionPerformedInBlazeProject(Project project, AnActionEvent e) {
     Library library = LibraryActionHelper.findLibraryForAction(e);
     if (library != null) {
       addDirectoriesToProjectView(project, ImmutableList.of(library));
@@ -59,18 +57,15 @@
   }
 
   @Override
-  protected void doUpdate(@NotNull AnActionEvent e) {
+  protected void updateForBlazeProject(Project project, AnActionEvent e) {
     Presentation presentation = e.getPresentation();
     boolean visible = false;
     boolean enabled = false;
-    Project project = e.getProject();
-    if (project != null) {
-      Library library = LibraryActionHelper.findLibraryForAction(e);
-      if (library != null) {
-        visible = true;
-        if (getDirectoryToAddForLibrary(project, library) != null) {
-          enabled = true;
-        }
+    Library library = LibraryActionHelper.findLibraryForAction(e);
+    if (library != null) {
+      visible = true;
+      if (getDirectoryToAddForLibrary(project, library) != null) {
+        enabled = true;
       }
     }
     presentation.setVisible(visible);
diff --git a/java/src/com/google/idea/blaze/java/libraries/AttachSourceJarAction.java b/java/src/com/google/idea/blaze/java/libraries/AttachSourceJarAction.java
index 7e18d28..9961f6f 100644
--- a/java/src/com/google/idea/blaze/java/libraries/AttachSourceJarAction.java
+++ b/java/src/com/google/idea/blaze/java/libraries/AttachSourceJarAction.java
@@ -15,7 +15,7 @@
  */
 package com.google.idea.blaze.java.libraries;
 
-import com.google.idea.blaze.base.actions.BlazeAction;
+import com.google.idea.blaze.base.actions.BlazeProjectAction;
 import com.google.idea.blaze.base.ideinfo.LibraryArtifact;
 import com.google.idea.blaze.base.model.BlazeProjectData;
 import com.google.idea.blaze.base.sync.data.BlazeProjectDataManager;
@@ -30,13 +30,11 @@
 import com.intellij.openapi.roots.libraries.Library;
 import com.intellij.openapi.roots.libraries.LibraryTable;
 import com.intellij.openapi.ui.Messages;
-import org.jetbrains.annotations.NotNull;
 
-class AttachSourceJarAction extends BlazeAction {
+class AttachSourceJarAction extends BlazeProjectAction {
+
   @Override
-  public void actionPerformed(AnActionEvent e) {
-    Project project = e.getProject();
-    assert project != null;
+  protected void actionPerformedInBlazeProject(Project project, AnActionEvent e) {
     BlazeProjectData blazeProjectData =
         BlazeProjectDataManager.getInstance(project).getBlazeProjectData();
     if (blazeProjectData == null) {
@@ -77,28 +75,25 @@
   }
 
   @Override
-  protected void doUpdate(@NotNull AnActionEvent e) {
+  protected void updateForBlazeProject(Project project, AnActionEvent e) {
     Presentation presentation = e.getPresentation();
     String text = "Attach Source Jar";
     boolean visible = false;
     boolean enabled = false;
-    Project project = e.getProject();
-    if (project != null) {
-      BlazeProjectData blazeProjectData =
-          BlazeProjectDataManager.getInstance(project).getBlazeProjectData();
-      if (blazeProjectData != null) {
-        Library library = LibraryActionHelper.findLibraryForAction(e);
-        if (library != null) {
-          visible = true;
+    BlazeProjectData blazeProjectData =
+        BlazeProjectDataManager.getInstance(project).getBlazeProjectData();
+    if (blazeProjectData != null) {
+      Library library = LibraryActionHelper.findLibraryForAction(e);
+      if (library != null) {
+        visible = true;
 
-          BlazeJarLibrary blazeLibrary =
-              LibraryActionHelper.findLibraryFromIntellijLibrary(
-                  e.getProject(), blazeProjectData, library);
-          if (blazeLibrary != null && blazeLibrary.libraryArtifact.sourceJar != null) {
-            enabled = true;
-            if (SourceJarManager.getInstance(project).hasSourceJarAttached(blazeLibrary.key)) {
-              text = "Detach Source Jar";
-            }
+        BlazeJarLibrary blazeLibrary =
+            LibraryActionHelper.findLibraryFromIntellijLibrary(
+                e.getProject(), blazeProjectData, library);
+        if (blazeLibrary != null && blazeLibrary.libraryArtifact.sourceJar != null) {
+          enabled = true;
+          if (SourceJarManager.getInstance(project).hasSourceJarAttached(blazeLibrary.key)) {
+            text = "Detach Source Jar";
           }
         }
       }
diff --git a/java/src/com/google/idea/blaze/java/libraries/BlazeAttachSourceProvider.java b/java/src/com/google/idea/blaze/java/libraries/BlazeAttachSourceProvider.java
index 05759ba..b5305b3 100644
--- a/java/src/com/google/idea/blaze/java/libraries/BlazeAttachSourceProvider.java
+++ b/java/src/com/google/idea/blaze/java/libraries/BlazeAttachSourceProvider.java
@@ -26,6 +26,7 @@
 import com.google.idea.blaze.base.sync.libraries.LibraryEditor;
 import com.google.idea.blaze.java.settings.BlazeJavaUserSettings;
 import com.google.idea.blaze.java.sync.model.BlazeJarLibrary;
+import com.google.idea.sdkcompat.transactions.Transactions;
 import com.intellij.codeInsight.AttachSourcesProvider;
 import com.intellij.openapi.application.ApplicationManager;
 import com.intellij.openapi.project.Project;
@@ -35,7 +36,6 @@
 import com.intellij.openapi.roots.libraries.LibraryTable;
 import com.intellij.openapi.util.ActionCallback;
 import com.intellij.psi.PsiFile;
-import com.intellij.util.ui.UIUtil;
 import java.util.Collection;
 import java.util.List;
 import org.jetbrains.annotations.NotNull;
@@ -85,7 +85,8 @@
      * corresponding user setting is active.
      */
     if (BlazeJavaUserSettings.getInstance().getAttachSourcesOnDemand()) {
-      UIUtil.invokeLaterIfNeeded(
+      Transactions.submitTransaction(
+          project,
           () -> {
             attachSources(project, blazeProjectData, librariesToAttachSourceTo);
           });
diff --git a/java/src/com/google/idea/blaze/java/libraries/DetachAllSourceJarsAction.java b/java/src/com/google/idea/blaze/java/libraries/DetachAllSourceJarsAction.java
index 10f70a3..37188f6 100644
--- a/java/src/com/google/idea/blaze/java/libraries/DetachAllSourceJarsAction.java
+++ b/java/src/com/google/idea/blaze/java/libraries/DetachAllSourceJarsAction.java
@@ -16,7 +16,7 @@
 package com.google.idea.blaze.java.libraries;
 
 import com.google.common.collect.Lists;
-import com.google.idea.blaze.base.actions.BlazeAction;
+import com.google.idea.blaze.base.actions.BlazeProjectAction;
 import com.google.idea.blaze.base.model.BlazeProjectData;
 import com.google.idea.blaze.base.model.LibraryKey;
 import com.google.idea.blaze.base.sync.data.BlazeProjectDataManager;
@@ -30,12 +30,10 @@
 import com.intellij.openapi.roots.libraries.LibraryTable;
 import java.util.List;
 
-class DetachAllSourceJarsAction extends BlazeAction {
-  @Override
-  public void actionPerformed(AnActionEvent e) {
-    Project project = e.getProject();
-    assert project != null;
+class DetachAllSourceJarsAction extends BlazeProjectAction {
 
+  @Override
+  protected void actionPerformedInBlazeProject(Project project, AnActionEvent e) {
     BlazeProjectData blazeProjectData =
         BlazeProjectDataManager.getInstance(project).getBlazeProjectData();
     if (blazeProjectData == null) {
diff --git a/java/src/com/google/idea/blaze/java/libraries/ExcludeLibraryAction.java b/java/src/com/google/idea/blaze/java/libraries/ExcludeLibraryAction.java
index 53d4ae8..48ab1d2 100644
--- a/java/src/com/google/idea/blaze/java/libraries/ExcludeLibraryAction.java
+++ b/java/src/com/google/idea/blaze/java/libraries/ExcludeLibraryAction.java
@@ -15,7 +15,7 @@
  */
 package com.google.idea.blaze.java.libraries;
 
-import com.google.idea.blaze.base.actions.BlazeAction;
+import com.google.idea.blaze.base.actions.BlazeProjectAction;
 import com.google.idea.blaze.base.ideinfo.LibraryArtifact;
 import com.google.idea.blaze.base.model.BlazeProjectData;
 import com.google.idea.blaze.base.projectview.ProjectViewEdit;
@@ -33,13 +33,11 @@
 import com.intellij.openapi.project.Project;
 import com.intellij.openapi.roots.libraries.Library;
 import com.intellij.openapi.ui.Messages;
-import org.jetbrains.annotations.NotNull;
 
-class ExcludeLibraryAction extends BlazeAction {
+class ExcludeLibraryAction extends BlazeProjectAction {
+
   @Override
-  public void actionPerformed(AnActionEvent e) {
-    Project project = e.getProject();
-    assert project != null;
+  protected void actionPerformedInBlazeProject(Project project, AnActionEvent e) {
     BlazeProjectData blazeProjectData =
         BlazeProjectDataManager.getInstance(project).getBlazeProjectData();
     if (blazeProjectData == null) {
@@ -88,7 +86,7 @@
   }
 
   @Override
-  protected void doUpdate(@NotNull AnActionEvent e) {
+  protected void updateForBlazeProject(Project project, AnActionEvent e) {
     Presentation presentation = e.getPresentation();
     boolean enabled = LibraryActionHelper.findLibraryForAction(e) != null;
     presentation.setVisible(enabled);
diff --git a/java/src/com/google/idea/blaze/java/run/BlazeJavaDebuggerRunner.java b/java/src/com/google/idea/blaze/java/run/BlazeJavaDebuggerRunner.java
index 001f26e..33a6540 100644
--- a/java/src/com/google/idea/blaze/java/run/BlazeJavaDebuggerRunner.java
+++ b/java/src/com/google/idea/blaze/java/run/BlazeJavaDebuggerRunner.java
@@ -17,7 +17,7 @@
 
 import com.google.idea.blaze.base.model.primitives.Kind;
 import com.google.idea.blaze.base.run.BlazeCommandRunConfiguration;
-import com.intellij.debugger.impl.GenericDebuggerRunner;
+import com.google.idea.sdkcompat.debugger.GenericDebuggerRunnerSdkCompatAdapter;
 import com.intellij.execution.ExecutionException;
 import com.intellij.execution.configurations.JavaParameters;
 import com.intellij.execution.configurations.RemoteConnection;
@@ -27,19 +27,21 @@
 import com.intellij.execution.executors.DefaultDebugExecutor;
 import com.intellij.execution.runners.ExecutionEnvironment;
 import com.intellij.execution.ui.RunContentDescriptor;
-import org.jetbrains.annotations.NotNull;
-import org.jetbrains.annotations.Nullable;
+import javax.annotation.Nullable;
 
 /** A runner that adapts the GenericDebuggerRunner to work with Blaze run configurations. */
-public class BlazeJavaDebuggerRunner extends GenericDebuggerRunner {
+public class BlazeJavaDebuggerRunner extends GenericDebuggerRunnerSdkCompatAdapter {
+
+  // wait 10 minutes for the blaze build to complete before connecting
+  private static final long POLL_TIMEOUT_MILLIS = 10 * 60 * 1000;
+
   @Override
-  @NotNull
   public String getRunnerId() {
     return "Blaze-Debug";
   }
 
   @Override
-  public boolean canRun(@NotNull final String executorId, @NotNull final RunProfile profile) {
+  public boolean canRun(final String executorId, final RunProfile profile) {
     if (executorId.equals(DefaultDebugExecutor.EXECUTOR_ID)
         && profile instanceof BlazeCommandRunConfiguration) {
       BlazeCommandRunConfiguration configuration = (BlazeCommandRunConfiguration) profile;
@@ -61,13 +63,12 @@
   @Override
   @Nullable
   public RunContentDescriptor createContentDescriptor(
-      @NotNull RunProfileState state, @NotNull ExecutionEnvironment environment)
-      throws ExecutionException {
+      RunProfileState state, ExecutionEnvironment environment) throws ExecutionException {
     if (!(state instanceof BlazeJavaRunProfileState)) {
       return null;
     }
     BlazeJavaRunProfileState blazeState = (BlazeJavaRunProfileState) state;
     RemoteConnection connection = blazeState.getRemoteConnection();
-    return attachVirtualMachine(state, environment, connection, true /* pollConnection */);
+    return attachVirtualMachine(state, environment, connection, POLL_TIMEOUT_MILLIS);
   }
 }
diff --git a/java/src/com/google/idea/blaze/java/run/BlazeJavaRunConfigurationHandler.java b/java/src/com/google/idea/blaze/java/run/BlazeJavaRunConfigurationHandler.java
index fd26ffd..479b964 100644
--- a/java/src/com/google/idea/blaze/java/run/BlazeJavaRunConfigurationHandler.java
+++ b/java/src/com/google/idea/blaze/java/run/BlazeJavaRunConfigurationHandler.java
@@ -22,6 +22,7 @@
 import com.google.idea.blaze.base.run.confighandler.BlazeCommandRunConfigurationRunner;
 import com.google.idea.blaze.base.run.state.BlazeCommandRunConfigurationCommonState;
 import com.google.idea.blaze.base.settings.Blaze;
+import com.google.idea.blaze.base.settings.Blaze.BuildSystem;
 import com.intellij.execution.Executor;
 import com.intellij.execution.configurations.RunConfiguration;
 import com.intellij.execution.configurations.RunProfileState;
@@ -38,8 +39,9 @@
   private final BlazeCommandRunConfigurationCommonState state;
 
   public BlazeJavaRunConfigurationHandler(BlazeCommandRunConfiguration configuration) {
-    this.buildSystemName = Blaze.buildSystemName(configuration.getProject());
-    this.state = new BlazeCommandRunConfigurationCommonState(buildSystemName);
+    BuildSystem buildSystem = Blaze.getBuildSystem(configuration.getProject());
+    this.buildSystemName = buildSystem.getName();
+    this.state = new BlazeCommandRunConfigurationCommonState(buildSystem);
   }
 
   @Override
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 a712e65..47b971b 100644
--- a/java/src/com/google/idea/blaze/java/run/BlazeJavaRunProfileState.java
+++ b/java/src/com/google/idea/blaze/java/run/BlazeJavaRunProfileState.java
@@ -28,8 +28,10 @@
 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.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.IdeaLogScope;
@@ -38,14 +40,22 @@
 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;
+import com.intellij.execution.Executor;
 import com.intellij.execution.configurations.CommandLineState;
 import com.intellij.execution.configurations.RemoteConnection;
 import com.intellij.execution.configurations.RemoteState;
 import com.intellij.execution.configurations.RunProfile;
+import com.intellij.execution.configurations.WrappingRunConfiguration;
+import com.intellij.execution.filters.TextConsoleBuilderImpl;
 import com.intellij.execution.process.ProcessHandler;
 import com.intellij.execution.process.ProcessListener;
 import com.intellij.execution.runners.ExecutionEnvironment;
+import com.intellij.execution.runners.ProgramRunner;
+import com.intellij.execution.ui.ConsoleView;
 import com.intellij.openapi.project.Project;
 
 /**
@@ -54,6 +64,10 @@
  * when using a debug executor.
  */
 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;
@@ -64,12 +78,18 @@
 
   public BlazeJavaRunProfileState(ExecutionEnvironment environment, boolean debug) {
     super(environment);
-    RunProfile runProfile = environment.getRunProfile();
-    assert runProfile instanceof BlazeCommandRunConfiguration;
-    configuration = (BlazeCommandRunConfiguration) runProfile;
+    this.configuration = getConfiguration(environment);
     this.debug = debug;
   }
 
+  private static BlazeCommandRunConfiguration getConfiguration(ExecutionEnvironment environment) {
+    RunProfile runProfile = environment.getRunProfile();
+    if (runProfile instanceof WrappingRunConfiguration) {
+      runProfile = ((WrappingRunConfiguration) runProfile).getPeer();
+    }
+    return (BlazeCommandRunConfiguration) runProfile;
+  }
+
   @Override
   protected ProcessHandler startProcess() throws ExecutionException {
     Project project = configuration.getProject();
@@ -80,7 +100,25 @@
     ProjectViewSet projectViewSet = ProjectViewManager.getInstance(project).getProjectViewSet();
     assert projectViewSet != null;
 
-    BlazeCommand blazeCommand = getBlazeCommand(project, configuration, projectViewSet, debug);
+    BlazeCommand blazeCommand;
+    if (useTestUi()) {
+      BlazeJavaTestEventsHandler eventsHandler = new BlazeJavaTestEventsHandler();
+      blazeCommand =
+          getBlazeCommand(
+              project, configuration, projectViewSet, eventsHandler.getBlazeFlags(), debug);
+      setConsoleBuilder(
+          new TextConsoleBuilderImpl(project) {
+            @Override
+            protected ConsoleView createConsole() {
+              return SmRunnerUtils.getConsoleView(
+                  project, configuration, getEnvironment().getExecutor(), eventsHandler);
+            }
+          });
+    } else {
+      blazeCommand =
+          getBlazeCommand(project, configuration, projectViewSet, ImmutableList.of(), debug);
+    }
+
     WorkspaceRoot workspaceRoot = WorkspaceRoot.fromImportSettings(importSettings);
     return new ScopedBlazeProcessHandler(
         project,
@@ -106,6 +144,24 @@
   }
 
   @Override
+  public ExecutionResult execute(Executor executor, ProgramRunner runner)
+      throws ExecutionException {
+    DefaultExecutionResult result = (DefaultExecutionResult) super.execute(executor, runner);
+    return SmRunnerUtils.attachRerunFailedTestsAction(result);
+  }
+
+  private boolean useTestUi() {
+    if (!smRunnerUiEnabled.getValue()) {
+      return false;
+    }
+    BlazeCommandRunConfigurationCommonState state =
+        configuration.getHandlerStateIfType(BlazeCommandRunConfigurationCommonState.class);
+    return state != null
+        && BlazeCommandName.TEST.equals(state.getCommand())
+        && !Boolean.TRUE.equals(state.getRunOnDistributedExecutor());
+  }
+
+  @Override
   public RemoteConnection getRemoteConnection() {
     if (!debug) {
       return null;
@@ -122,6 +178,7 @@
       Project project,
       BlazeCommandRunConfiguration configuration,
       ProjectViewSet projectViewSet,
+      ImmutableList<String> extraBlazeFlags,
       boolean debug) {
 
     BlazeCommandRunConfigurationCommonState handlerState =
@@ -135,6 +192,7 @@
             .setBlazeBinary(handlerState.getBlazeBinary())
             .addTargets(configuration.getTarget())
             .addBlazeFlags(BlazeFlags.buildFlags(project, projectViewSet))
+            .addBlazeFlags(extraBlazeFlags)
             .addBlazeFlags(handlerState.getBlazeFlags());
 
     if (debug) {
@@ -145,6 +203,10 @@
       } else {
         command.addBlazeFlags(BlazeFlags.JAVA_TEST_DEBUG);
       }
+    } else {
+      command.addBlazeFlags(
+          DistributedExecutorSupport.getBlazeFlags(
+              project, handlerState.getRunOnDistributedExecutor()));
     }
 
     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
new file mode 100644
index 0000000..9af9b64
--- /dev/null
+++ b/java/src/com/google/idea/blaze/java/run/BlazeJavaTestEventsHandler.java
@@ -0,0 +1,94 @@
+/*
+ * 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.java.run;
+
+import com.google.idea.blaze.base.command.BlazeFlags;
+import com.google.idea.blaze.base.run.smrunner.BlazeTestEventsHandler;
+import com.google.idea.blaze.java.run.producers.BlazeJUnitTestFilterFlags;
+import com.intellij.execution.Location;
+import com.intellij.execution.testframework.AbstractTestProxy;
+import com.intellij.execution.testframework.JavaTestLocator;
+import com.intellij.execution.testframework.sm.runner.SMTestLocator;
+import com.intellij.openapi.project.Project;
+import com.intellij.openapi.util.text.StringUtil;
+import com.intellij.psi.PsiClass;
+import com.intellij.psi.PsiElement;
+import com.intellij.psi.PsiMethod;
+import com.intellij.psi.search.GlobalSearchScope;
+import com.intellij.util.containers.MultiMap;
+import com.intellij.util.io.URLUtil;
+import java.util.List;
+import javax.annotation.Nullable;
+
+/** Provides java-specific methods needed by the SM-runner test UI. */
+public class BlazeJavaTestEventsHandler extends BlazeTestEventsHandler {
+
+  public BlazeJavaTestEventsHandler() {
+    super("Blaze Java Test");
+  }
+
+  @Override
+  public SMTestLocator getTestLocator() {
+    return JavaTestLocator.INSTANCE;
+  }
+
+  @Override
+  public String suiteLocationUrl(String name) {
+    return JavaTestLocator.SUITE_PROTOCOL + URLUtil.SCHEME_SEPARATOR + name;
+  }
+
+  @Override
+  public String testLocationUrl(String name, @Nullable String classname) {
+    if (classname == null) {
+      return null;
+    }
+    return JavaTestLocator.TEST_PROTOCOL + URLUtil.SCHEME_SEPARATOR + classname + "." + name;
+  }
+
+  @Override
+  public String suiteDisplayName(String rawName) {
+    String name = StringUtil.trimEnd(rawName, '.');
+    int lastPointIx = name.lastIndexOf('.');
+    return lastPointIx != -1 ? name.substring(lastPointIx + 1, name.length()) : name;
+  }
+
+  @Nullable
+  @Override
+  public String getTestFilter(Project project, List<AbstractTestProxy> failedTests) {
+    GlobalSearchScope projectScope = GlobalSearchScope.allScope(project);
+    MultiMap<PsiClass, PsiMethod> failedMethodsPerClass = new MultiMap<>();
+    for (AbstractTestProxy test : failedTests) {
+      appendTest(failedMethodsPerClass, test.getLocation(project, projectScope));
+    }
+    String filter = BlazeJUnitTestFilterFlags.testFilterForClassesAndMethods(failedMethodsPerClass);
+    return filter != null ? BlazeFlags.TEST_FILTER + "=" + filter : null;
+  }
+
+  private static void appendTest(
+      MultiMap<PsiClass, PsiMethod> testMap, @Nullable Location<?> testLocation) {
+    if (testLocation == null) {
+      return;
+    }
+    PsiElement method = testLocation.getPsiElement();
+    if (!(method instanceof PsiMethod)) {
+      return;
+    }
+    PsiClass psiClass = ((PsiMethod) method).getContainingClass();
+    if (psiClass != null) {
+      testMap.putValue(psiClass, (PsiMethod) method);
+    }
+  }
+}
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 2c6a6aa..8f2290f 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
@@ -16,76 +16,147 @@
 
 package com.google.idea.blaze.java.run.producers;
 
+import com.google.common.annotations.VisibleForTesting;
 import com.google.common.base.Strings;
-import com.google.idea.blaze.base.command.BlazeFlags;
-import com.google.idea.common.experiments.BoolExperiment;
+import com.google.common.collect.ImmutableList;
+import com.intellij.execution.junit.JUnitUtil;
+import com.intellij.execution.junit2.PsiMemberParameterizedLocation;
+import com.intellij.psi.PsiClass;
+import com.intellij.psi.PsiMethod;
+import com.intellij.util.containers.MultiMap;
 import java.util.Collection;
+import java.util.List;
+import java.util.Map.Entry;
+import java.util.stream.Collectors;
 import javax.annotation.Nullable;
 
 /** Utilities for building test filter flags for JUnit tests. */
 public final class BlazeJUnitTestFilterFlags {
 
-  private static final BoolExperiment enableParameterizedSupport =
-      new BoolExperiment("enable.parameterized.test.support", true);
-
   /** A version of JUnit to generate test filter flags for. */
   public enum JUnitVersion {
     JUNIT_3,
     JUNIT_4
   }
 
-  public static String testFilterFlagForClass(String className, JUnitVersion jUnitVersion) {
-    return testFilterFlagForClassAndMethod(
-        className, null, jUnitVersion, /* parameterized doesn't matter for a class */ false);
+  /**
+   * Builds the JUnit test filter corresponding to the given class.<br>
+   * Returns null if no class name can be found.
+   */
+  @Nullable
+  public static String testFilterForClass(PsiClass psiClass) {
+    return testFilterForClassAndMethods(psiClass, ImmutableList.of());
   }
 
-  public static String testFilterFlagForClassAndMethod(
+  /**
+   * Builds the JUnit test filter corresponding to the given class and methods.<br>
+   * Returns null if no class name can be found.
+   */
+  @Nullable
+  public static String testFilterForClassAndMethods(
+      PsiClass psiClass, Collection<PsiMethod> methods) {
+    JUnitVersion version =
+        JUnitUtil.isJUnit4TestClass(psiClass) ? JUnitVersion.JUNIT_4 : JUnitVersion.JUNIT_3;
+    return testFilterForClassAndMethods(psiClass, version, methods);
+  }
+
+  @Nullable
+  public static String testFilterForClassesAndMethods(
+      MultiMap<PsiClass, PsiMethod> methodsPerClass) {
+    // Note: this could be incorrect if there are no JUnit4 classes in this sample, but some in the
+    // java_test target they're run from.
+    JUnitVersion version =
+        hasJUnit4Test(methodsPerClass.keySet()) ? JUnitVersion.JUNIT_4 : JUnitVersion.JUNIT_3;
+    return testFilterForClassesAndMethods(methodsPerClass, version);
+  }
+
+  @Nullable
+  public static String testFilterForClassesAndMethods(
+      MultiMap<PsiClass, PsiMethod> methodsPerClass, JUnitVersion version) {
+    StringBuilder output = new StringBuilder();
+    for (Entry<PsiClass, Collection<PsiMethod>> entry : methodsPerClass.entrySet()) {
+      String filter = testFilterForClassAndMethods(entry.getKey(), version, entry.getValue());
+      if (filter != null) {
+        output.append(filter);
+      }
+    }
+    return Strings.emptyToNull(output.toString());
+  }
+
+  private static boolean hasJUnit4Test(Collection<PsiClass> classes) {
+    for (PsiClass psiClass : classes) {
+      if (JUnitUtil.isJUnit4TestClass(psiClass)) {
+        return true;
+      }
+    }
+    return false;
+  }
+
+  /**
+   * Builds the JUnit test filter corresponding to the given class and methods.<br>
+   * Returns null if no class name can be found.
+   */
+  @Nullable
+  private static String testFilterForClassAndMethods(
+      PsiClass psiClass, JUnitVersion version, Collection<PsiMethod> methods) {
+    String className = psiClass.getQualifiedName();
+    if (className == null) {
+      return null;
+    }
+    // Sort so multiple configurations created with different selection orders are the same.
+    List<String> methodNames =
+        methods.stream().map(PsiMethod::getName).sorted().collect(Collectors.toList());
+    return testFilterForClassAndMethods(className, methodNames, version, isParameterized(psiClass));
+  }
+
+  private static boolean isParameterized(PsiClass testClass) {
+    return PsiMemberParameterizedLocation.getParameterizedLocation(testClass, null) != null;
+  }
+
+  /**
+   * Builds the blaze test_filter flag for JUnit tests. Excludes the "--test_filter" component of
+   * the flag, so that multiple test classes can be combined.
+   */
+  @VisibleForTesting
+  static String testFilterForClassAndMethods(
       String className,
-      @Nullable String methodName,
+      List<String> methodNames,
       JUnitVersion jUnitVersion,
       boolean parameterized) {
-    StringBuilder output = new StringBuilder(BlazeFlags.TEST_FILTER);
-    output.append('=');
-    output.append(className);
-
-    if (!Strings.isNullOrEmpty(methodName)) {
-      output.append('#');
-      output.append(methodName);
-      // JUnit 4 test filters are regexes, and must be terminated to avoid matching
-      // unintended classes/methods. JUnit 3 test filters do not need or support this syntax.
+    StringBuilder output = new StringBuilder(className);
+    String methodNamePattern = concatenateMethodNames(methodNames, jUnitVersion);
+    if (Strings.isNullOrEmpty(methodNamePattern)) {
       if (jUnitVersion == JUnitVersion.JUNIT_4) {
-        // parameterized tests include their parameters between brackets after the method name
-        if (parameterized && enableParameterizedSupport.getValue()) {
-          output.append("(\\[.+\\])?");
-        }
-        output.append("$");
+        output.append('#');
       }
-    } else if (jUnitVersion == JUnitVersion.JUNIT_4) {
-      output.append('#');
+      return output.toString();
     }
-
+    output.append('#').append(methodNamePattern);
+    // JUnit 4 test filters are regexes, and must be terminated to avoid matching
+    // unintended classes/methods. JUnit 3 test filters do not need or support this syntax.
+    if (jUnitVersion == JUnitVersion.JUNIT_3) {
+      return output.toString();
+    }
+    // parameterized tests include their parameters between brackets after the method name
+    if (parameterized) {
+      output.append("(\\[.+\\])?");
+    }
+    output.append('$');
     return output.toString();
   }
 
-  public static String testFilterFlagForClassAndMethods(
-      String className,
-      Collection<String> methodNames,
-      JUnitVersion jUnitVersion,
-      boolean parameterized) {
-    if (methodNames.size() == 0) {
-      return testFilterFlagForClass(className, jUnitVersion);
-    } else if (methodNames.size() == 1) {
-      return testFilterFlagForClassAndMethod(
-          className, methodNames.iterator().next(), jUnitVersion, parameterized);
+  @Nullable
+  private static String concatenateMethodNames(
+      List<String> methodNames, JUnitVersion jUnitVersion) {
+    if (methodNames.isEmpty()) {
+      return null;
     }
-    String methodNamePattern;
-    if (jUnitVersion == JUnitVersion.JUNIT_4) {
-      methodNamePattern = String.format("(%s)", String.join("|", methodNames));
-    } else {
-      methodNamePattern = String.join(",", methodNames);
+    if (methodNames.size() == 1) {
+      return methodNames.get(0);
     }
-    return testFilterFlagForClassAndMethod(
-        className, methodNamePattern, jUnitVersion, parameterized);
+    return jUnitVersion == JUnitVersion.JUNIT_4
+        ? String.format("(%s)", String.join("|", methodNames))
+        : String.join(",", methodNames);
   }
 
   private BlazeJUnitTestFilterFlags() {}
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 3eb8ab3..95bc4ee 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
@@ -26,7 +26,6 @@
 import com.google.idea.blaze.base.run.producers.BlazeRunConfigurationProducer;
 import com.google.idea.blaze.base.run.state.BlazeCommandRunConfigurationCommonState;
 import com.google.idea.blaze.java.run.RunUtil;
-import com.google.idea.blaze.java.run.producers.BlazeJUnitTestFilterFlags.JUnitVersion;
 import com.intellij.execution.JavaExecutionUtil;
 import com.intellij.execution.Location;
 import com.intellij.execution.actions.ConfigurationContext;
@@ -36,7 +35,6 @@
 import com.intellij.psi.PsiElement;
 import com.intellij.psi.PsiMethod;
 import com.intellij.psi.PsiModifier;
-import java.util.List;
 import java.util.Objects;
 import org.jetbrains.annotations.NotNull;
 
@@ -90,13 +88,10 @@
 
     ImmutableList.Builder<String> flags = ImmutableList.builder();
 
-    final String qualifiedName = testClass.getQualifiedName();
-    if (qualifiedName != null) {
-      final JUnitVersion jUnitVersion =
-          JUnitUtil.isJUnit4TestClass(testClass) ? JUnitVersion.JUNIT_4 : JUnitVersion.JUNIT_3;
-      flags.add(BlazeJUnitTestFilterFlags.testFilterFlagForClass(qualifiedName, jUnitVersion));
+    String testFilter = BlazeJUnitTestFilterFlags.testFilterForClass(testClass);
+    if (testFilter != null) {
+      flags.add(BlazeFlags.TEST_FILTER + "=" + testFilter);
     }
-
     flags.add(BlazeFlags.TEST_OUTPUT_STREAMED);
     flags.addAll(handlerState.getBlazeFlags());
 
@@ -147,12 +142,7 @@
     if (!Objects.equals(handlerState.getCommand(), BlazeCommandName.TEST)) {
       return false;
     }
-    List<String> flags = handlerState.getBlazeFlags();
-
-    final JUnitVersion jUnitVersion =
-        JUnitUtil.isJUnit4TestClass(testClass) ? JUnitVersion.JUNIT_4 : JUnitVersion.JUNIT_3;
-    return flags.contains(
-        BlazeJUnitTestFilterFlags.testFilterFlagForClass(
-            testClass.getQualifiedName(), jUnitVersion));
+    String filter = BlazeJUnitTestFilterFlags.testFilterForClass(testClass);
+    return Objects.equals(filter, handlerState.getTestFilterFlag());
   }
 }
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 2fc899f..1a7e149 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
@@ -26,18 +26,14 @@
 import com.google.idea.blaze.base.run.producers.BlazeRunConfigurationProducer;
 import com.google.idea.blaze.base.run.state.BlazeCommandRunConfigurationCommonState;
 import com.google.idea.blaze.java.run.RunUtil;
-import com.google.idea.blaze.java.run.producers.BlazeJUnitTestFilterFlags.JUnitVersion;
 import com.intellij.execution.actions.ConfigurationContext;
-import com.intellij.execution.junit.JUnitUtil;
-import com.intellij.execution.junit2.PsiMemberParameterizedLocation;
 import com.intellij.openapi.util.Ref;
 import com.intellij.psi.PsiClass;
 import com.intellij.psi.PsiElement;
 import com.intellij.psi.PsiMethod;
-import java.util.ArrayList;
-import java.util.Collections;
 import java.util.List;
 import java.util.Objects;
+import java.util.stream.Collectors;
 import org.jetbrains.annotations.NotNull;
 
 /** Producer for run configurations related to Java test methods in Blaze. */
@@ -152,29 +148,15 @@
         return null;
       }
     }
-
-    final List<String> methodNames = new ArrayList<>();
-    for (PsiMethod method : selectedMethods) {
-      methodNames.add(method.getName());
-    }
-    // Sort so multiple configurations created with different selection orders are the same.
-    Collections.sort(methodNames);
-
-    final String qualifiedName = containingClass.getQualifiedName();
-    if (qualifiedName == null) {
+    String testFilter =
+        BlazeJUnitTestFilterFlags.testFilterForClassAndMethods(containingClass, selectedMethods);
+    if (testFilter == null) {
       return null;
     }
-    final JUnitVersion jUnitVersion =
-        JUnitUtil.isJUnit4TestClass(containingClass) ? JUnitVersion.JUNIT_4 : JUnitVersion.JUNIT_3;
-    boolean parameterized = isParameterized(containingClass);
-    final String testFilterFlag =
-        BlazeJUnitTestFilterFlags.testFilterFlagForClassAndMethods(
-            qualifiedName, methodNames, jUnitVersion, parameterized);
-
+    // Sort so multiple configurations created with different selection orders are the same.
+    List<String> methodNames =
+        selectedMethods.stream().map(PsiMethod::getName).sorted().collect(Collectors.toList());
+    final String testFilterFlag = BlazeFlags.TEST_FILTER + "=" + testFilter;
     return new SelectedMethodInfo(firstMethod, containingClass, methodNames, testFilterFlag);
   }
-
-  private static boolean isParameterized(PsiClass testClass) {
-    return PsiMemberParameterizedLocation.getParameterizedLocation(testClass, null) != null;
-  }
 }
diff --git a/java/src/com/google/idea/blaze/java/sync/BlazeJavaSyncPlugin.java b/java/src/com/google/idea/blaze/java/sync/BlazeJavaSyncPlugin.java
index 65151a6..b971b94 100644
--- a/java/src/com/google/idea/blaze/java/sync/BlazeJavaSyncPlugin.java
+++ b/java/src/com/google/idea/blaze/java/sync/BlazeJavaSyncPlugin.java
@@ -22,6 +22,7 @@
 import com.google.idea.blaze.base.ideinfo.TargetMap;
 import com.google.idea.blaze.base.model.BlazeLibrary;
 import com.google.idea.blaze.base.model.BlazeProjectData;
+import com.google.idea.blaze.base.model.BlazeVersionData;
 import com.google.idea.blaze.base.model.SyncState;
 import com.google.idea.blaze.base.model.primitives.LanguageClass;
 import com.google.idea.blaze.base.model.primitives.WorkspaceRoot;
@@ -56,6 +57,7 @@
 import com.google.idea.blaze.java.sync.projectstructure.JavaSourceFolderProvider;
 import com.google.idea.blaze.java.sync.projectstructure.Jdks;
 import com.google.idea.blaze.java.sync.workingset.JavaWorkingSet;
+import com.google.idea.sdkcompat.transactions.Transactions;
 import com.intellij.openapi.application.ApplicationManager;
 import com.intellij.openapi.module.ModuleType;
 import com.intellij.openapi.module.StdModuleTypes;
@@ -64,7 +66,6 @@
 import com.intellij.openapi.roots.LanguageLevelProjectExtension;
 import com.intellij.openapi.roots.ex.ProjectRootManagerEx;
 import com.intellij.pom.java.LanguageLevel;
-import com.intellij.util.ui.UIUtil;
 import java.util.Collection;
 import java.util.Set;
 import javax.annotation.Nullable;
@@ -170,6 +171,7 @@
       Project project,
       BlazeContext context,
       ProjectViewSet projectViewSet,
+      BlazeVersionData blazeVersionData,
       BlazeProjectData blazeProjectData) {
     if (!blazeProjectData.workspaceLanguageSettings.isWorkspaceType(WorkspaceType.JAVA)) {
       return;
@@ -212,19 +214,18 @@
 
   private static void setProjectSdkAndLanguageLevel(
       final Project project, final Sdk sdk, final LanguageLevel javaLanguageLevel) {
-    UIUtil.invokeAndWaitIfNeeded(
-        (Runnable)
-            () ->
-                ApplicationManager.getApplication()
-                    .runWriteAction(
-                        () -> {
-                          ProjectRootManagerEx rootManager =
-                              ProjectRootManagerEx.getInstanceEx(project);
-                          rootManager.setProjectSdk(sdk);
-                          LanguageLevelProjectExtension ext =
-                              LanguageLevelProjectExtension.getInstance(project);
-                          ext.setLanguageLevel(javaLanguageLevel);
-                        }));
+    Transactions.submitTransactionAndWait(
+        () ->
+            ApplicationManager.getApplication()
+                .runWriteAction(
+                    () -> {
+                      ProjectRootManagerEx rootManager =
+                          ProjectRootManagerEx.getInstanceEx(project);
+                      rootManager.setProjectSdk(sdk);
+                      LanguageLevelProjectExtension ext =
+                          LanguageLevelProjectExtension.getInstance(project);
+                      ext.setLanguageLevel(javaLanguageLevel);
+                    }));
   }
 
   @Override
diff --git a/java/src/com/google/idea/blaze/java/sync/projectstructure/JavaSourceFolderProvider.java b/java/src/com/google/idea/blaze/java/sync/projectstructure/JavaSourceFolderProvider.java
index 5855655..abe5804 100644
--- a/java/src/com/google/idea/blaze/java/sync/projectstructure/JavaSourceFolderProvider.java
+++ b/java/src/com/google/idea/blaze/java/sync/projectstructure/JavaSourceFolderProvider.java
@@ -23,6 +23,7 @@
 import com.google.idea.blaze.java.sync.model.BlazeJavaSyncData;
 import com.google.idea.blaze.java.sync.model.BlazeSourceDirectory;
 import com.intellij.openapi.application.ApplicationManager;
+import com.intellij.openapi.diagnostic.Logger;
 import com.intellij.openapi.roots.ContentEntry;
 import com.intellij.openapi.roots.SourceFolder;
 import com.intellij.openapi.util.io.FileUtil;
@@ -42,6 +43,7 @@
 
 /** Edits source folders in IntelliJ content entries */
 public class JavaSourceFolderProvider implements SourceFolderProvider {
+  private static final Logger logger = Logger.getInstance(JavaSourceFolderProvider.class);
 
   private final ImmutableMap<File, BlazeContentEntry> blazeContentEntries;
 
@@ -106,6 +108,7 @@
 
   private static String derivePackagePrefix(VirtualFile file, SourceFolder parentFolder) {
     String parentPackagePrefix = parentFolder.getPackagePrefix();
+    logger.assertTrue(parentFolder.getFile() != null);
     String relativePath = VfsUtilCore.getRelativePath(file, parentFolder.getFile(), '.');
     if (Strings.isNullOrEmpty(relativePath)) {
       return parentPackagePrefix;
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 3f3aede..10e43eb 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
@@ -29,6 +29,7 @@
 import com.google.idea.blaze.base.projectview.ProjectViewSet;
 import com.google.idea.blaze.base.run.BlazeCommandRunConfiguration;
 import com.google.idea.blaze.base.run.BlazeCommandRunConfigurationType;
+import com.google.idea.blaze.base.run.DistributedExecutorSupport;
 import com.google.idea.blaze.base.run.confighandler.BlazeCommandGenericRunConfigurationHandlerProvider;
 import com.google.idea.blaze.base.run.confighandler.BlazeCommandRunConfigurationHandlerProvider;
 import com.google.idea.blaze.base.run.state.BlazeCommandRunConfigurationCommonState;
@@ -68,6 +69,7 @@
     applicationServices.register(TargetFinder.class, new MockTargetFinder());
     applicationServices.register(BlazeUserSettings.class, new BlazeUserSettings());
     registerExtensionPoint(BuildFlagsProvider.EP_NAME, BuildFlagsProvider.class);
+    registerExtensionPoint(DistributedExecutorSupport.EP_NAME, DistributedExecutorSupport.class);
     ExtensionPointImpl<BlazeCommandRunConfigurationHandlerProvider> handlerProviderEp =
         registerExtensionPoint(
             BlazeCommandRunConfigurationHandlerProvider.EP_NAME,
@@ -87,7 +89,11 @@
     handlerState.setBlazeFlags(ImmutableList.of("--flag1", "--flag2"));
     assertThat(
             BlazeJavaRunProfileState.getBlazeCommand(
-                    project, configuration, ProjectViewSet.builder().build(), false /* debug */)
+                    project,
+                    configuration,
+                    ProjectViewSet.builder().build(),
+                    ImmutableList.of(),
+                    false /* debug */)
                 .toList())
         .isEqualTo(
             ImmutableList.of(
@@ -108,7 +114,11 @@
     handlerState.setCommand(BlazeCommandName.fromString("command"));
     assertThat(
             BlazeJavaRunProfileState.getBlazeCommand(
-                    project, configuration, ProjectViewSet.builder().build(), true /* debug */)
+                    project,
+                    configuration,
+                    ProjectViewSet.builder().build(),
+                    ImmutableList.of(),
+                    true /* debug */)
                 .toList())
         .isEqualTo(
             ImmutableList.of(
@@ -128,7 +138,11 @@
     handlerState.setCommand(BlazeCommandName.fromString("command"));
     assertThat(
             BlazeJavaRunProfileState.getBlazeCommand(
-                    project, configuration, ProjectViewSet.builder().build(), true /* debug */)
+                    project,
+                    configuration,
+                    ProjectViewSet.builder().build(),
+                    ImmutableList.of(),
+                    true /* debug */)
                 .toList())
         .isEqualTo(
             ImmutableList.of(
diff --git a/java/tests/unittests/com/google/idea/blaze/java/run/producers/BlazeJUnitTestFilterFlagsTest.java b/java/tests/unittests/com/google/idea/blaze/java/run/producers/BlazeJUnitTestFilterFlagsTest.java
new file mode 100644
index 0000000..d5d2cde
--- /dev/null
+++ b/java/tests/unittests/com/google/idea/blaze/java/run/producers/BlazeJUnitTestFilterFlagsTest.java
@@ -0,0 +1,130 @@
+/*
+ * 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.java.run.producers;
+
+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.java.run.producers.BlazeJUnitTestFilterFlags.JUnitVersion;
+import com.google.idea.common.experiments.ExperimentService;
+import com.google.idea.common.experiments.MockExperimentService;
+import org.jetbrains.annotations.NotNull;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** Unit tests for {@link BlazeJUnitTestFilterFlags}. */
+@RunWith(JUnit4.class)
+public class BlazeJUnitTestFilterFlagsTest extends BlazeTestCase {
+
+  @Override
+  protected void initTest(
+      @NotNull Container applicationServices, @NotNull Container projectServices) {
+    ExperimentService experimentService = new MockExperimentService();
+    applicationServices.register(ExperimentService.class, experimentService);
+  }
+
+  @Test
+  public void testSingleJUnit4ClassFilter() {
+    assertThat(
+            BlazeJUnitTestFilterFlags.testFilterForClassAndMethods(
+                "com.google.idea.ClassName", ImmutableList.of(), JUnitVersion.JUNIT_4, false))
+        .isEqualTo("com.google.idea.ClassName#");
+  }
+
+  @Test
+  public void testSingleJUnit3ClassFilter() {
+    assertThat(
+            BlazeJUnitTestFilterFlags.testFilterForClassAndMethods(
+                "com.google.idea.ClassName", ImmutableList.of(), JUnitVersion.JUNIT_3, false))
+        .isEqualTo("com.google.idea.ClassName");
+  }
+
+  @Test
+  public void testParameterizedIgnoredForSingleClass() {
+    assertThat(
+            BlazeJUnitTestFilterFlags.testFilterForClassAndMethods(
+                "com.google.idea.ClassName", ImmutableList.of(), JUnitVersion.JUNIT_4, true))
+        .isEqualTo("com.google.idea.ClassName#");
+  }
+
+  @Test
+  public void testJUnit4ClassAndSingleMethod() {
+    assertThat(
+            BlazeJUnitTestFilterFlags.testFilterForClassAndMethods(
+                "com.google.idea.ClassName",
+                ImmutableList.of("testMethod1"),
+                JUnitVersion.JUNIT_4,
+                false))
+        .isEqualTo("com.google.idea.ClassName#testMethod1$");
+  }
+
+  @Test
+  public void testJUnit3ClassAndSingleMethod() {
+    assertThat(
+            BlazeJUnitTestFilterFlags.testFilterForClassAndMethods(
+                "com.google.idea.ClassName",
+                ImmutableList.of("testMethod1"),
+                JUnitVersion.JUNIT_3,
+                false))
+        .isEqualTo("com.google.idea.ClassName#testMethod1");
+  }
+
+  @Test
+  public void testJUnit4ClassAndMultipleMethods() {
+    assertThat(
+            BlazeJUnitTestFilterFlags.testFilterForClassAndMethods(
+                "com.google.idea.ClassName",
+                ImmutableList.of("testMethod1", "testMethod2"),
+                JUnitVersion.JUNIT_4,
+                false))
+        .isEqualTo("com.google.idea.ClassName#(testMethod1|testMethod2)$");
+  }
+
+  @Test
+  public void testJUnit4ParametrizedClassAndMultipleMethods() {
+    assertThat(
+            BlazeJUnitTestFilterFlags.testFilterForClassAndMethods(
+                "com.google.idea.ClassName",
+                ImmutableList.of("testMethod1", "testMethod2"),
+                JUnitVersion.JUNIT_4,
+                true))
+        .isEqualTo("com.google.idea.ClassName#(testMethod1|testMethod2)(\\[.+\\])?$");
+  }
+
+  @Test
+  public void testJUnit3ClassAndMultipleMethods() {
+    assertThat(
+            BlazeJUnitTestFilterFlags.testFilterForClassAndMethods(
+                "com.google.idea.ClassName",
+                ImmutableList.of("testMethod1", "testMethod2"),
+                JUnitVersion.JUNIT_3,
+                false))
+        .isEqualTo("com.google.idea.ClassName#testMethod1,testMethod2");
+  }
+
+  @Test
+  public void testParameterizedIgnoredForJUnit3() {
+    assertThat(
+            BlazeJUnitTestFilterFlags.testFilterForClassAndMethods(
+                "com.google.idea.ClassName",
+                ImmutableList.of("testMethod1", "testMethod2"),
+                JUnitVersion.JUNIT_3,
+                true))
+        .isEqualTo("com.google.idea.ClassName#testMethod1,testMethod2");
+  }
+}
diff --git a/plugin_dev/BUILD b/plugin_dev/BUILD
index 48eb552..1c56953 100644
--- a/plugin_dev/BUILD
+++ b/plugin_dev/BUILD
@@ -9,6 +9,7 @@
         "//intellij_platform_sdk:devkit",
         "//intellij_platform_sdk:plugin_api",
         "//java",
+        "//sdkcompat",
         "@jsr305_annotations//jar",
     ],
 )
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 00cbccb..a68f89b 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
@@ -35,6 +35,8 @@
 import com.google.idea.blaze.base.projectview.ProjectViewSet;
 import com.google.idea.blaze.base.run.BlazeConfigurationNameBuilder;
 import com.google.idea.blaze.base.run.BlazeRunConfiguration;
+import com.google.idea.blaze.base.run.state.RunConfigurationFlagsState;
+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.sync.data.BlazeProjectDataManager;
@@ -47,6 +49,7 @@
 import com.intellij.execution.configurations.JavaCommandLineState;
 import com.intellij.execution.configurations.JavaParameters;
 import com.intellij.execution.configurations.LocatableConfigurationBase;
+import com.intellij.execution.configurations.LogFileOptions;
 import com.intellij.execution.configurations.ModuleRunConfiguration;
 import com.intellij.execution.configurations.ParametersList;
 import com.intellij.execution.configurations.RunProfileState;
@@ -75,13 +78,15 @@
 import com.intellij.openapi.util.WriteExternalException;
 import com.intellij.ui.ListCellRendererWrapper;
 import com.intellij.ui.RawCommandLineEditor;
+import com.intellij.ui.components.JBCheckBox;
 import com.intellij.util.PlatformUtils;
-import com.intellij.util.execution.ParametersListUtil;
 import java.awt.BorderLayout;
 import java.io.File;
 import java.io.IOException;
 import java.nio.file.Files;
+import java.nio.file.Paths;
 import java.nio.file.StandardCopyOption;
+import java.util.ArrayList;
 import java.util.Collection;
 import java.util.List;
 import javax.annotation.Nullable;
@@ -89,7 +94,6 @@
 import javax.swing.JComponent;
 import javax.swing.JLabel;
 import javax.swing.JList;
-import javax.swing.JTextArea;
 import org.jdom.Element;
 
 /**
@@ -105,16 +109,20 @@
   private static final String SDK_ATTR = "blaze-plugin-sdk";
   private static final String VM_PARAMS_ATTR = "blaze-vm-params";
   private static final String PROGRAM_PARAMS_ATTR = "blaze-program-params";
+  private static final String KEEP_IN_SYNC_TAG = "keep-in-sync";
 
   private final String buildSystem;
 
   @Nullable private Label target;
-  private ImmutableList<String> blazeFlags = ImmutableList.of();
-  private ImmutableList<String> exeFlags = ImmutableList.of();
+  private final RunConfigurationFlagsState blazeFlags;
+  private final RunConfigurationFlagsState exeFlags;
   @Nullable private Sdk pluginSdk;
   @Nullable String vmParameters;
   @Nullable private String programParameters;
 
+  // for keeping imported configurations in sync with their source XML
+  @Nullable private Boolean keepInSync = null;
+
   public BlazeIntellijPluginConfiguration(
       Project project,
       ConfigurationFactory factory,
@@ -129,6 +137,19 @@
     if (initialTarget != null) {
       target = initialTarget.key.label;
     }
+    blazeFlags = new RunConfigurationFlagsState(USER_BLAZE_FLAG_TAG, buildSystem + " flags:");
+    exeFlags = new RunConfigurationFlagsState(USER_EXE_FLAG_TAG, "Executable flags:");
+  }
+
+  @Override
+  public void setKeepInSync(@Nullable Boolean keepInSync) {
+    this.keepInSync = keepInSync;
+  }
+
+  @Override
+  @Nullable
+  public Boolean getKeepInSync() {
+    return keepInSync;
   }
 
   @Override
@@ -147,6 +168,19 @@
     }
   }
 
+  @Override
+  public ArrayList<LogFileOptions> getAllLogFiles() {
+    ArrayList<LogFileOptions> result = new ArrayList<>();
+    if (pluginSdk == null) {
+      return result;
+    }
+    String sandboxHome = IdeaJdkHelper.getSandboxHome(pluginSdk);
+    String logFile = Paths.get(sandboxHome, "system", "log", "idea.log").toString();
+    LogFileOptions logFileOptions = new LogFileOptions("idea.log", logFile, true, true, true);
+    result.add(logFileOptions);
+    return result;
+  }
+
   private ImmutableList<File> findPluginJars() throws ExecutionException {
     BlazeProjectData blazeProjectData =
         BlazeProjectDataManager.getInstance(getProject()).getBlazeProjectData();
@@ -353,8 +387,8 @@
     } else {
       target = null;
     }
-    blazeFlags = loadUserFlags(element, USER_BLAZE_FLAG_TAG);
-    exeFlags = loadUserFlags(element, USER_EXE_FLAG_TAG);
+    blazeFlags.readExternal(element);
+    exeFlags.readExternal(element);
 
     String sdkName = element.getAttributeValue(SDK_ATTR);
     if (!Strings.isNullOrEmpty(sdkName)) {
@@ -362,25 +396,9 @@
     }
     vmParameters = Strings.emptyToNull(element.getAttributeValue(VM_PARAMS_ATTR));
     programParameters = Strings.emptyToNull(element.getAttributeValue(PROGRAM_PARAMS_ATTR));
-  }
 
-  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();
-  }
-
-  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);
-    }
+    String keepInSyncString = element.getAttributeValue(KEEP_IN_SYNC_TAG);
+    keepInSync = keepInSyncString != null ? Boolean.parseBoolean(keepInSyncString) : null;
   }
 
   @Override
@@ -392,8 +410,8 @@
       targetElement.setText(target.toString());
       element.addContent(targetElement);
     }
-    saveUserFlags(element, blazeFlags, USER_BLAZE_FLAG_TAG);
-    saveUserFlags(element, exeFlags, USER_EXE_FLAG_TAG);
+    blazeFlags.writeExternal(element);
+    exeFlags.writeExternal(element);
     if (pluginSdk != null) {
       element.setAttribute(SDK_ATTR, pluginSdk.getName());
     }
@@ -403,6 +421,9 @@
     if (programParameters != null) {
       element.setAttribute(PROGRAM_PARAMS_ATTR, programParameters);
     }
+    if (keepInSync != null) {
+      element.setAttribute(KEEP_IN_SYNC_TAG, Boolean.toString(keepInSync));
+    }
   }
 
   @Override
@@ -410,11 +431,12 @@
     final BlazeIntellijPluginConfiguration configuration =
         (BlazeIntellijPluginConfiguration) super.clone();
     configuration.target = target;
-    configuration.blazeFlags = blazeFlags;
-    configuration.exeFlags = exeFlags;
+    configuration.blazeFlags.setFlags(blazeFlags.getFlags());
+    configuration.exeFlags.setFlags(exeFlags.getFlags());
     configuration.pluginSdk = pluginSdk;
     configuration.vmParameters = vmParameters;
     configuration.programParameters = programParameters;
+    configuration.keepInSync = keepInSync;
     return configuration;
   }
 
@@ -423,8 +445,8 @@
         BlazeCommand.builder(Blaze.getBuildSystem(getProject()), BlazeCommandName.BUILD)
             .addTargets(getTarget())
             .addBlazeFlags(BlazeFlags.buildFlags(project, projectViewSet))
-            .addBlazeFlags(blazeFlags)
-            .addExeFlags(exeFlags);
+            .addBlazeFlags(blazeFlags.getFlags())
+            .addExeFlags(exeFlags.getFlags());
     return command.build();
   }
 
@@ -436,7 +458,8 @@
     for (TargetIdeInfo target : javaTargets) {
       javaLabels.add(target.key.label);
     }
-    return new BlazeIntellijPluginConfigurationSettingsEditor(buildSystem, javaLabels);
+    return new BlazeIntellijPluginConfigurationSettingsEditor(
+        javaLabels, blazeFlags.getEditor(getProject()), exeFlags.getEditor(getProject()));
   }
 
   @Override
@@ -456,21 +479,23 @@
   @VisibleForTesting
   static class BlazeIntellijPluginConfigurationSettingsEditor
       extends SettingsEditor<BlazeIntellijPluginConfiguration> {
-    private final String buildSystemName;
-    private final ComboBox targetCombo;
-    private final JTextArea blazeFlagsField = new JTextArea(5, 0);
-    private final JTextArea exeFlagsField = new JTextArea(5, 0);
+    private final ComboBox<Label> targetCombo;
+    private final RunConfigurationStateEditor blazeFlagsEditor;
+    private final RunConfigurationStateEditor exeFlagsEditor;
     private final JdkComboBox sdkCombo;
     private final LabeledComponent<RawCommandLineEditor> vmParameters = new LabeledComponent<>();
     private final LabeledComponent<RawCommandLineEditor> programParameters =
         new LabeledComponent<>();
+    private final JBCheckBox keepInSyncCheckBox;
 
     public BlazeIntellijPluginConfigurationSettingsEditor(
-        String buildSystemName, List<Label> javaLabels) {
-      this.buildSystemName = buildSystemName;
+        List<Label> javaLabels,
+        RunConfigurationStateEditor blazeFlagsEditor,
+        RunConfigurationStateEditor exeFlagsEditor) {
       targetCombo =
-          new ComboBox(
-              new DefaultComboBoxModel(Ordering.usingToString().sortedCopy(javaLabels).toArray()));
+          new ComboBox<>(
+              new DefaultComboBoxModel<>(
+                  Ordering.usingToString().sortedCopy(javaLabels).toArray(new Label[0])));
       targetCombo.setRenderer(
           new ListCellRendererWrapper<Label>() {
             @Override
@@ -479,18 +504,35 @@
               setText(value == null ? null : value.toString());
             }
           });
-
+      this.blazeFlagsEditor = blazeFlagsEditor;
+      this.exeFlagsEditor = exeFlagsEditor;
       ProjectSdksModel sdksModel = new ProjectSdksModel();
       sdksModel.reset(null);
       sdkCombo = new JdkComboBox(sdksModel, IdeaJdkHelper::isIdeaJdkType);
+
+      keepInSyncCheckBox = new JBCheckBox("Keep in sync with source XML");
+      keepInSyncCheckBox.addItemListener(e -> updateEnabledStatus());
+    }
+
+    private void updateEnabledStatus() {
+      setEnabled(!keepInSyncCheckBox.isVisible() || !keepInSyncCheckBox.isSelected());
+    }
+
+    private void setEnabled(boolean enabled) {
+      targetCombo.setEnabled(enabled);
+      sdkCombo.setEnabled(enabled);
+      vmParameters.getComponent().setEnabled(enabled);
+      programParameters.getComponent().setEnabled(enabled);
+      blazeFlagsEditor.setComponentEnabled(enabled);
+      exeFlagsEditor.setComponentEnabled(enabled);
     }
 
     @VisibleForTesting
     @Override
     public void resetEditorFrom(BlazeIntellijPluginConfiguration s) {
       targetCombo.setSelectedItem(s.getTarget());
-      blazeFlagsField.setText(ParametersListUtil.join(s.blazeFlags));
-      exeFlagsField.setText(ParametersListUtil.join(s.exeFlags));
+      blazeFlagsEditor.resetEditorFrom(s.blazeFlags);
+      exeFlagsEditor.resetEditorFrom(s.exeFlags);
       if (s.pluginSdk != null) {
         sdkCombo.setSelectedJdk(s.pluginSdk);
       } else {
@@ -502,6 +544,10 @@
       if (s.programParameters != null) {
         programParameters.getComponent().setText(s.programParameters);
       }
+      keepInSyncCheckBox.setVisible(s.keepInSync != null);
+      if (s.keepInSync != null) {
+        keepInSyncCheckBox.setSelected(s.keepInSync);
+      }
     }
 
     @VisibleForTesting
@@ -512,15 +558,12 @@
       } catch (ClassCastException e) {
         throw new ConfigurationException("Invalid label specified.");
       }
-      s.blazeFlags =
-          ImmutableList.copyOf(
-              ParametersListUtil.parse(Strings.nullToEmpty(blazeFlagsField.getText())));
-      s.exeFlags =
-          ImmutableList.copyOf(
-              ParametersListUtil.parse(Strings.nullToEmpty(exeFlagsField.getText())));
+      blazeFlagsEditor.applyEditorTo(s.blazeFlags);
+      exeFlagsEditor.applyEditorTo(s.exeFlags);
       s.pluginSdk = sdkCombo.getSelectedJdk();
       s.vmParameters = vmParameters.getComponent().getText();
       s.programParameters = programParameters.getComponent().getText();
+      s.keepInSync = keepInSyncCheckBox.isVisible() ? keepInSyncCheckBox.isSelected() : null;
     }
 
     @Override
@@ -544,10 +587,9 @@
           vmParameters.getComponent(),
           programParameters.getLabel(),
           programParameters.getComponent(),
-          new JLabel(buildSystemName + " flags:"),
-          blazeFlagsField,
-          new JLabel("Executable flags:"),
-          exeFlagsField);
+          blazeFlagsEditor.createComponent(),
+          exeFlagsEditor.createComponent(),
+          keepInSyncCheckBox);
     }
   }
 }
diff --git a/plugin_dev/src/com/google/idea/blaze/plugin/sync/IntellijPluginSyncPlugin.java b/plugin_dev/src/com/google/idea/blaze/plugin/sync/IntellijPluginSyncPlugin.java
index 34b06df..e2eeb8e 100644
--- a/plugin_dev/src/com/google/idea/blaze/plugin/sync/IntellijPluginSyncPlugin.java
+++ b/plugin_dev/src/com/google/idea/blaze/plugin/sync/IntellijPluginSyncPlugin.java
@@ -18,6 +18,7 @@
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableSet;
 import com.google.idea.blaze.base.model.BlazeProjectData;
+import com.google.idea.blaze.base.model.BlazeVersionData;
 import com.google.idea.blaze.base.model.primitives.LanguageClass;
 import com.google.idea.blaze.base.model.primitives.WorkspaceType;
 import com.google.idea.blaze.base.projectview.ProjectViewSet;
@@ -27,13 +28,13 @@
 import com.google.idea.blaze.java.sync.JavaLanguageLevelHelper;
 import com.google.idea.blaze.java.sync.model.BlazeJavaSyncData;
 import com.google.idea.blaze.java.sync.projectstructure.JavaSourceFolderProvider;
+import com.google.idea.sdkcompat.transactions.Transactions;
 import com.intellij.openapi.application.ApplicationManager;
 import com.intellij.openapi.module.ModuleType;
 import com.intellij.openapi.module.StdModuleTypes;
 import com.intellij.openapi.project.Project;
 import com.intellij.openapi.roots.LanguageLevelProjectExtension;
 import com.intellij.pom.java.LanguageLevel;
-import com.intellij.util.ui.UIUtil;
 import java.util.Set;
 import javax.annotation.Nullable;
 
@@ -79,6 +80,7 @@
       Project project,
       BlazeContext context,
       ProjectViewSet projectViewSet,
+      BlazeVersionData blazeVersionData,
       BlazeProjectData blazeProjectData) {
     if (!blazeProjectData.workspaceLanguageSettings.isWorkspaceType(
         WorkspaceType.INTELLIJ_PLUGIN)) {
@@ -90,15 +92,14 @@
             projectViewSet, blazeProjectData, LanguageLevel.JDK_1_7);
 
     // Leave the SDK, but set the language level
-    UIUtil.invokeAndWaitIfNeeded(
-        (Runnable)
-            () ->
-                ApplicationManager.getApplication()
-                    .runWriteAction(
-                        () -> {
-                          LanguageLevelProjectExtension ext =
-                              LanguageLevelProjectExtension.getInstance(project);
-                          ext.setLanguageLevel(javaLanguageLevel);
-                        }));
+    Transactions.submitTransactionAndWait(
+        () ->
+            ApplicationManager.getApplication()
+                .runWriteAction(
+                    () -> {
+                      LanguageLevelProjectExtension ext =
+                          LanguageLevelProjectExtension.getInstance(project);
+                      ext.setLanguageLevel(javaLanguageLevel);
+                    }));
   }
 }
diff --git a/proto_deps/proto_deps.jar b/proto_deps/proto_deps.jar
index 1a140af..6f79519 100755
--- a/proto_deps/proto_deps.jar
+++ b/proto_deps/proto_deps.jar
Binary files differ
diff --git a/sdkcompat/BUILD b/sdkcompat/BUILD
new file mode 100644
index 0000000..ce19ce5
--- /dev/null
+++ b/sdkcompat/BUILD
@@ -0,0 +1,25 @@
+# Description: Indirections for SDK changes to the underlying platform library.
+
+licenses(["notice"])  # Apache 2.0
+
+load("//intellij_platform_sdk:build_defs.bzl", "select_for_plugin_api")
+
+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/**"],
+        ),
+    }),
+    visibility = ["//visibility:public"],
+    deps = [
+        "//intellij_platform_sdk:plugin_api",
+        "@jsr305_annotations//jar",
+    ],
+)
diff --git a/sdkcompat/v145/com/google/idea/sdkcompat/codestyle/CodeStyleManagerSdkCompatAdapter.java b/sdkcompat/v145/com/google/idea/sdkcompat/codestyle/CodeStyleManagerSdkCompatAdapter.java
new file mode 100644
index 0000000..6051cc9
--- /dev/null
+++ b/sdkcompat/v145/com/google/idea/sdkcompat/codestyle/CodeStyleManagerSdkCompatAdapter.java
@@ -0,0 +1,6 @@
+package com.google.idea.sdkcompat.codestyle;
+
+import com.intellij.psi.codeStyle.CodeStyleManager;
+
+/** Adapter to extend two bridge different IntelliJ SDK versions. */
+public abstract class CodeStyleManagerSdkCompatAdapter extends CodeStyleManager {}
diff --git a/sdkcompat/v145/com/google/idea/sdkcompat/debugger/GenericDebuggerRunnerSdkCompatAdapter.java b/sdkcompat/v145/com/google/idea/sdkcompat/debugger/GenericDebuggerRunnerSdkCompatAdapter.java
new file mode 100644
index 0000000..6dfc995
--- /dev/null
+++ b/sdkcompat/v145/com/google/idea/sdkcompat/debugger/GenericDebuggerRunnerSdkCompatAdapter.java
@@ -0,0 +1,25 @@
+package com.google.idea.sdkcompat.debugger;
+
+import com.intellij.debugger.impl.GenericDebuggerRunner;
+import com.intellij.execution.ExecutionException;
+import com.intellij.execution.configurations.RemoteConnection;
+import com.intellij.execution.configurations.RunProfileState;
+import com.intellij.execution.runners.ExecutionEnvironment;
+import com.intellij.execution.ui.RunContentDescriptor;
+import javax.annotation.Nullable;
+
+/** SDK compatibility for {@link GenericDebuggerRunner}. */
+public class GenericDebuggerRunnerSdkCompatAdapter extends GenericDebuggerRunner {
+
+  @Nullable
+  protected RunContentDescriptor attachVirtualMachine(
+      RunProfileState state,
+      ExecutionEnvironment env,
+      RemoteConnection connection,
+      long pollTimeout)
+      throws ExecutionException {
+    // no timeout available until 2016.2 onwards
+    return super.attachVirtualMachine(
+        state, env, connection, pollTimeout != 0 /* pollConnection */);
+  }
+}
diff --git a/sdkcompat/v145/com/google/idea/sdkcompat/smrunner/SmRunnerCompatUtils.java b/sdkcompat/v145/com/google/idea/sdkcompat/smrunner/SmRunnerCompatUtils.java
new file mode 100644
index 0000000..c4bd9a7
--- /dev/null
+++ b/sdkcompat/v145/com/google/idea/sdkcompat/smrunner/SmRunnerCompatUtils.java
@@ -0,0 +1,13 @@
+package com.google.idea.sdkcompat.smrunner;
+
+import com.intellij.execution.testframework.sm.runner.events.TestFailedEvent;
+import javax.annotation.Nullable;
+
+/** Handles SM-runner methods which have changed between our supported versions. */
+public class SmRunnerCompatUtils {
+
+  public static TestFailedEvent getTestFailedEvent(
+      String name, @Nullable String message, @Nullable String content, long duration) {
+    return new TestFailedEvent(name, -1, message, content, true, null, null, null, duration);
+  }
+}
diff --git a/sdkcompat/v145/com/google/idea/sdkcompat/transactions/Transactions.java b/sdkcompat/v145/com/google/idea/sdkcompat/transactions/Transactions.java
new file mode 100644
index 0000000..94ab905
--- /dev/null
+++ b/sdkcompat/v145/com/google/idea/sdkcompat/transactions/Transactions.java
@@ -0,0 +1,16 @@
+package com.google.idea.sdkcompat.transactions;
+
+import com.intellij.openapi.Disposable;
+import com.intellij.openapi.application.ApplicationManager;
+import com.intellij.openapi.application.ModalityState;
+
+/** Created by tomlu on 12/22/16. */
+public class Transactions {
+  public static void submitTransactionAndWait(Runnable runnable) {
+    ApplicationManager.getApplication().invokeAndWait(runnable, ModalityState.any());
+  }
+
+  public static void submitTransaction(Disposable disposable, Runnable runnable) {
+    ApplicationManager.getApplication().invokeLater(runnable);
+  }
+}
diff --git a/sdkcompat/v145/com/google/idea/sdkcompat/vcs/ChangeListManagerSdkCompatAdapter.java b/sdkcompat/v145/com/google/idea/sdkcompat/vcs/ChangeListManagerSdkCompatAdapter.java
new file mode 100644
index 0000000..df055ea
--- /dev/null
+++ b/sdkcompat/v145/com/google/idea/sdkcompat/vcs/ChangeListManagerSdkCompatAdapter.java
@@ -0,0 +1,12 @@
+package com.google.idea.sdkcompat.vcs;
+
+import com.intellij.openapi.vcs.changes.ChangeListManager;
+import com.intellij.util.continuation.ContinuationPause;
+
+/** SDK adapter for change list interface. */
+public abstract class ChangeListManagerSdkCompatAdapter extends ChangeListManager {
+  @Override
+  public void freeze(ContinuationPause context, String reason) {
+    throw new UnsupportedOperationException("ChangeListManager#freeze()");
+  }
+}
diff --git a/sdkcompat/v162/com/google/idea/sdkcompat/codestyle/CodeStyleManagerSdkCompatAdapter.java b/sdkcompat/v162/com/google/idea/sdkcompat/codestyle/CodeStyleManagerSdkCompatAdapter.java
new file mode 100644
index 0000000..6051cc9
--- /dev/null
+++ b/sdkcompat/v162/com/google/idea/sdkcompat/codestyle/CodeStyleManagerSdkCompatAdapter.java
@@ -0,0 +1,6 @@
+package com.google.idea.sdkcompat.codestyle;
+
+import com.intellij.psi.codeStyle.CodeStyleManager;
+
+/** Adapter to extend two bridge different IntelliJ SDK versions. */
+public abstract class CodeStyleManagerSdkCompatAdapter extends CodeStyleManager {}
diff --git a/sdkcompat/v162/com/google/idea/sdkcompat/debugger/GenericDebuggerRunnerSdkCompatAdapter.java b/sdkcompat/v162/com/google/idea/sdkcompat/debugger/GenericDebuggerRunnerSdkCompatAdapter.java
new file mode 100644
index 0000000..ed822b3
--- /dev/null
+++ b/sdkcompat/v162/com/google/idea/sdkcompat/debugger/GenericDebuggerRunnerSdkCompatAdapter.java
@@ -0,0 +1,6 @@
+package com.google.idea.sdkcompat.debugger;
+
+import com.intellij.debugger.impl.GenericDebuggerRunner;
+
+/** SDK compatibility for {@link GenericDebuggerRunner}. */
+public class GenericDebuggerRunnerSdkCompatAdapter extends GenericDebuggerRunner {}
diff --git a/sdkcompat/v162/com/google/idea/sdkcompat/smrunner/SmRunnerCompatUtils.java b/sdkcompat/v162/com/google/idea/sdkcompat/smrunner/SmRunnerCompatUtils.java
new file mode 100644
index 0000000..1e9b474
--- /dev/null
+++ b/sdkcompat/v162/com/google/idea/sdkcompat/smrunner/SmRunnerCompatUtils.java
@@ -0,0 +1,13 @@
+package com.google.idea.sdkcompat.smrunner;
+
+import com.intellij.execution.testframework.sm.runner.events.TestFailedEvent;
+import javax.annotation.Nullable;
+
+/** Handles SM-runner methods which have changed between our supported versions. */
+public class SmRunnerCompatUtils {
+
+  public static TestFailedEvent getTestFailedEvent(
+      String name, @Nullable String message, @Nullable String content, long duration) {
+    return new TestFailedEvent(name, null, message, content, true, null, null, null, duration);
+  }
+}
diff --git a/sdkcompat/v162/com/google/idea/sdkcompat/transactions/Transactions.java b/sdkcompat/v162/com/google/idea/sdkcompat/transactions/Transactions.java
new file mode 100644
index 0000000..94ab905
--- /dev/null
+++ b/sdkcompat/v162/com/google/idea/sdkcompat/transactions/Transactions.java
@@ -0,0 +1,16 @@
+package com.google.idea.sdkcompat.transactions;
+
+import com.intellij.openapi.Disposable;
+import com.intellij.openapi.application.ApplicationManager;
+import com.intellij.openapi.application.ModalityState;
+
+/** Created by tomlu on 12/22/16. */
+public class Transactions {
+  public static void submitTransactionAndWait(Runnable runnable) {
+    ApplicationManager.getApplication().invokeAndWait(runnable, ModalityState.any());
+  }
+
+  public static void submitTransaction(Disposable disposable, Runnable runnable) {
+    ApplicationManager.getApplication().invokeLater(runnable);
+  }
+}
diff --git a/sdkcompat/v162/com/google/idea/sdkcompat/vcs/ChangeListManagerSdkCompatAdapter.java b/sdkcompat/v162/com/google/idea/sdkcompat/vcs/ChangeListManagerSdkCompatAdapter.java
new file mode 100644
index 0000000..df055ea
--- /dev/null
+++ b/sdkcompat/v162/com/google/idea/sdkcompat/vcs/ChangeListManagerSdkCompatAdapter.java
@@ -0,0 +1,12 @@
+package com.google.idea.sdkcompat.vcs;
+
+import com.intellij.openapi.vcs.changes.ChangeListManager;
+import com.intellij.util.continuation.ContinuationPause;
+
+/** SDK adapter for change list interface. */
+public abstract class ChangeListManagerSdkCompatAdapter extends ChangeListManager {
+  @Override
+  public void freeze(ContinuationPause context, String reason) {
+    throw new UnsupportedOperationException("ChangeListManager#freeze()");
+  }
+}
diff --git a/sdkcompat/v163/com/google/idea/sdkcompat/codestyle/CodeStyleManagerSdkCompatAdapter.java b/sdkcompat/v163/com/google/idea/sdkcompat/codestyle/CodeStyleManagerSdkCompatAdapter.java
new file mode 100644
index 0000000..33d3ad4
--- /dev/null
+++ b/sdkcompat/v163/com/google/idea/sdkcompat/codestyle/CodeStyleManagerSdkCompatAdapter.java
@@ -0,0 +1,22 @@
+package com.google.idea.sdkcompat.codestyle;
+
+import com.intellij.openapi.util.TextRange;
+import com.intellij.psi.PsiFile;
+import com.intellij.psi.codeStyle.ChangedRangesInfo;
+import com.intellij.psi.codeStyle.CodeStyleManager;
+import com.intellij.util.IncorrectOperationException;
+import java.util.ArrayList;
+import java.util.List;
+import org.jetbrains.annotations.NotNull;
+
+/** Adapter to extend two bridge different IntelliJ SDK versions. */
+public abstract class CodeStyleManagerSdkCompatAdapter extends CodeStyleManager {
+  @Override
+  public void reformatTextWithContext(@NotNull PsiFile file, @NotNull ChangedRangesInfo info)
+      throws IncorrectOperationException {
+    List<TextRange> ranges = new ArrayList<>();
+    ranges.addAll(info.insertedRanges);
+    ranges.addAll(info.allChangedRanges);
+    this.reformatTextWithContext(file, ranges);
+  }
+}
diff --git a/sdkcompat/v163/com/google/idea/sdkcompat/debugger/GenericDebuggerRunnerSdkCompatAdapter.java b/sdkcompat/v163/com/google/idea/sdkcompat/debugger/GenericDebuggerRunnerSdkCompatAdapter.java
new file mode 100644
index 0000000..ed822b3
--- /dev/null
+++ b/sdkcompat/v163/com/google/idea/sdkcompat/debugger/GenericDebuggerRunnerSdkCompatAdapter.java
@@ -0,0 +1,6 @@
+package com.google.idea.sdkcompat.debugger;
+
+import com.intellij.debugger.impl.GenericDebuggerRunner;
+
+/** SDK compatibility for {@link GenericDebuggerRunner}. */
+public class GenericDebuggerRunnerSdkCompatAdapter extends GenericDebuggerRunner {}
diff --git a/sdkcompat/v163/com/google/idea/sdkcompat/smrunner/SmRunnerCompatUtils.java b/sdkcompat/v163/com/google/idea/sdkcompat/smrunner/SmRunnerCompatUtils.java
new file mode 100644
index 0000000..1e9b474
--- /dev/null
+++ b/sdkcompat/v163/com/google/idea/sdkcompat/smrunner/SmRunnerCompatUtils.java
@@ -0,0 +1,13 @@
+package com.google.idea.sdkcompat.smrunner;
+
+import com.intellij.execution.testframework.sm.runner.events.TestFailedEvent;
+import javax.annotation.Nullable;
+
+/** Handles SM-runner methods which have changed between our supported versions. */
+public class SmRunnerCompatUtils {
+
+  public static TestFailedEvent getTestFailedEvent(
+      String name, @Nullable String message, @Nullable String content, long duration) {
+    return new TestFailedEvent(name, null, message, content, true, null, null, null, duration);
+  }
+}
diff --git a/sdkcompat/v163/com/google/idea/sdkcompat/transactions/Transactions.java b/sdkcompat/v163/com/google/idea/sdkcompat/transactions/Transactions.java
new file mode 100644
index 0000000..8862aa8
--- /dev/null
+++ b/sdkcompat/v163/com/google/idea/sdkcompat/transactions/Transactions.java
@@ -0,0 +1,15 @@
+package com.google.idea.sdkcompat.transactions;
+
+import com.intellij.openapi.Disposable;
+import com.intellij.openapi.application.TransactionGuard;
+
+/** SDK adapter to use transaction guards. */
+public class Transactions {
+  public static void submitTransactionAndWait(Runnable runnable) {
+    TransactionGuard.getInstance().submitTransactionAndWait(runnable);
+  }
+
+  public static void submitTransaction(Disposable disposable, Runnable runnable) {
+    TransactionGuard.submitTransaction(disposable, runnable);
+  }
+}
diff --git a/sdkcompat/v163/com/google/idea/sdkcompat/vcs/ChangeListManagerSdkCompatAdapter.java b/sdkcompat/v163/com/google/idea/sdkcompat/vcs/ChangeListManagerSdkCompatAdapter.java
new file mode 100644
index 0000000..1e525c1
--- /dev/null
+++ b/sdkcompat/v163/com/google/idea/sdkcompat/vcs/ChangeListManagerSdkCompatAdapter.java
@@ -0,0 +1,6 @@
+package com.google.idea.sdkcompat.vcs;
+
+import com.intellij.openapi.vcs.changes.ChangeListManager;
+
+/** SDK adapter to for changelist interface. */
+public abstract class ChangeListManagerSdkCompatAdapter extends ChangeListManager {}
diff --git a/version.bzl b/version.bzl
index 0f144e5..1bcca22 100644
--- a/version.bzl
+++ b/version.bzl
@@ -1,3 +1,3 @@
 """Version of the blaze plugin."""
 
-VERSION = "2016.12.05.6"
+VERSION = "2017.01.09.1"