Merge pull request #90 from brendandouglas/master

Import of bazel plugin using copybara
diff --git a/BUILD b/BUILD
index 99282bc..5e26650 100644
--- a/BUILD
+++ b/BUILD
@@ -4,6 +4,13 @@
 
 licenses(["notice"])  # Apache 2.0
 
+# Changelog file
+filegroup(
+    name = "changelog",
+    srcs = ["CHANGELOG"],
+    visibility = ["//:__subpackages__"],
+)
+
 # IJwB tests, run with an IntelliJ plugin SDK
 test_suite(
     name = "ijwb_tests",
@@ -16,6 +23,10 @@
         "//java:integration_tests",
         "//java:unit_tests",
         "//plugin_dev:integration_tests",
+        "//python:integration_tests",
+        "//python:unit_tests",
+        "//scala:integration_tests",
+        "//scala:unit_tests",
     ],
 )
 
@@ -38,5 +49,6 @@
     tests = [
         "//base:unit_tests",
         "//cpp:unit_tests",
+        "//python:unit_tests",
     ],
 )
diff --git a/CHANGELOG b/CHANGELOG
new file mode 100644
index 0000000..32034b6
--- /dev/null
+++ b/CHANGELOG
@@ -0,0 +1,101 @@
+v2017.05.08
+===========
+* Add Python support to CLion
+* Fix some bazel targets not being linkified in the run configuration console
+  output
+* Add an action to open a workspace file outside your project (File > Open
+  Workspace File...)
+* Add an action to add a source directory to your bazel project (Bazel >
+  Project > Add Directory To Project...)
+* CLion: fix project directories being cleared when reopening a project
+
+v2017.04.17
+===========
+* Add support for IntelliJ 2017.1
+* Support bazel build sharding for large projects
+* Detect out-of-memory errors during sync, and suggest enabling sharding
+* Add documentation links for .bazelproject items
+
+v2017.04.03
+===========
+* Add python support for IntelliJ
+* Prefetch project files on project open, prior to initial indexing
+* Handle nested junit test classes
+
+v2017.03.15
+===========
+* Bazel: WORKSPACE file language integration (syntax highlighting, navigation,
+  etc.)
+* Bazel: Find usages, navigation support for external workspace labels
+* Expand macros in run configuration build flags
+
+v2017.02.27
+===========
+* Add CLion support
+* Run configuration support for abstract test classes/methods
+* Support running all test classes in a directory
+* BUILD support: don't suggest private symbols in 'load' statement autocomplete
+
+v2017.02.13
+===========
+* Test UI support for parameterized tests
+* Test UI support for sharded tests, run locally
+* BUILD: Fix navigation for overridden built-in symbols
+* BUILD: Add auto-complete for fully-qualified class names
+
+v2017.01.30
+============
+* Integrate bazel test results with the IDE's test runner UI.
+* Add support for sharing run configurations
+* Restructure Bazel menu items
+
+v2017.01.09
+===========
+* Create source roots for all directories matching 'test_sources'.
+* When viewing source files for supported but inactive languages, suggest
+  enabling support for that language.
+* BUILD: Add syntax highlight/autocomplete support for more built-in functions.
+* Fix java debugger connection timeout
+* Basic support for Go-lang projects
+
+v2016.12.5
+==========
+* BUILD files: add syntax hightlighting for built-in names
+* BUILD files: support aliased load statements
+* ASwB: enable NDK support
+
+v1.12
+=====
+* Add autocomplete in run configuration target editor.
+* Fix debugging of java_binary targets with args
+
+v1.11
+=====
+* Completely suppress JUnit for Bazel projects, removing a common source of
+  confusion.
+* Improve sync working set / partial sync to include more targets that
+  users might expect should be included.
+* Add more history to import wizard.
+
+v1.10
+=====
+* Compatibility with 2016.2.4
+* Improve create run configuration from scratch experience
+
+v1.9
+==========
+* Better tolerance of broken BUILD files during sync
+* Sync working set action -- sync only the files you're
+  working on.
+* BUILD file support: performance improvements.
+* Unified run configurations -- there is only one type,
+  the Bazel Command Run Configuration.
+* Add test rule chooser heuristics, to support some common
+  test genrules.
+
+v1.8
+==========
+* Add local jar cache to improve performance and robustness.
+* Support filtered gen jars to allow mixed generated/non-
+  generated rules (requires bazel release to activate).
+* Abbreviate generated run configuration names.
diff --git a/README.md b/README.md
index a6ba730..439b85e 100644
--- a/README.md
+++ b/README.md
@@ -13,8 +13,8 @@
 ## Installation
 
 You can find our plugin in the Jetbrains plugin repository by going to
-`Settings -> Browse Repositories`, and searching for `IntelliJ with Bazel`
-or `Android Studio with Bazel`.
+`Settings -> Browse Repositories`, and searching for `IntelliJ with Bazel`,
+`Android Studio with Bazel`, or `CLion with Bazel`.
 
 ## Usage
 
@@ -25,6 +25,6 @@
 
 ## Building the plugin
 
-Install Bazel, then run 'bazel build //ijwb:ijwb_bazel --define=ij_product=intellij-latest'
+Install Bazel, then run `bazel build //ijwb:ijwb_bazel --define=ij_product=intellij-latest`
 from the project root. This will create a plugin jar in
-'bazel-genfiles/ijwb/ijwb_bazel.jar'.
+`bazel-genfiles/ijwb/ijwb_bazel.jar`.
diff --git a/WORKSPACE b/WORKSPACE
index bad62f5..9a4eaff 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 2017.1.1. This is required to build IJwB,
+# and run integration tests.
+new_http_archive(
+    name = "intellij_ce_2017_1_1",
+    build_file = "intellij_platform_sdk/BUILD.idea",
+    url = "https://www.jetbrains.com/intellij-repository/releases/com/jetbrains/intellij/idea/ideaIC/2017.1.1/ideaIC-2017.1.1.zip",
+)
+
 # The plugin api for IntelliJ 2016.3.1. This is required to build IJwB,
 # and run integration tests.
 new_http_archive(
@@ -34,28 +42,62 @@
     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,
+# The plugin api for CLion 2017.1.1. This is required to build CLwB,
 # 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",
+    name = "clion_2017_1_1",
+    build_file = "intellij_platform_sdk/BUILD.clion",
+    url = "https://download.jetbrains.com/cpp/CLion-2017.1.1.tar.gz",
 )
 
-# The plugin api for Android Studio 2.3 Beta 2. This is required to build ASwB,
+# The plugin api for Android Studio 2.3.1. This is required to build ASwB,
 # and run integration tests.
 new_http_archive(
-    name = "android_studio_2_3_0_4",
+    name = "android_studio_2_3_1_0",
     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",
+    url = "https://dl.google.com/dl/android/studio/ide-zips/2.3.1.0/android-studio-ide-162.3871768-linux.zip",
 )
 
-# The plugin api for Android Studio 2.2 stable. This is required to build ASwB,
-# and run integration tests.
+# Python plugin for IntelliJ CE 2016.3. Required at compile-time for python-specific features.
 new_http_archive(
-    name = "AI_145_1617_8",
-    build_file = "intellij_platform_sdk/BUILD.android_studio",
-    url = "https://dl.google.com/dl/android/studio/ide-zips/2.2.0.12/android-studio-ide-145.3276617-linux.zip",
+    name = "python_2016_3",
+    build_file_content = "\n".join([
+        "java_import(",
+        "    name = 'python',",
+        "    jars = ['python/lib/python.jar'],",
+        "    visibility = ['//visibility:public'],",
+        ")"]),
+    url = "https://plugins.jetbrains.com/files/7322/32326/python-community-163.298.zip",
+)
+
+# Python plugin for IntelliJ CE 2017.1. Required at compile-time for python-specific features.
+new_http_archive(
+    name = "python_2017_1",
+    build_file_content = "\n".join([
+        "java_import(",
+        "    name = 'python',",
+        "    jars = ['python-ce/lib/python-ce.jar'],",
+        "    visibility = ['//visibility:public'],",
+        ")"]),
+    url = "https://plugins.jetbrains.com/files/7322/33704/python-ce-2017.1.171.3780.116.zip",
+)
+
+# Scala plugin for IntelliJ CE 2017.1. Required at compile-time for scala-specific features.
+new_http_archive(
+    name = "scala_2017_1",
+    build_file_content = "\n".join([
+        "java_import(",
+        "    name = 'scala-library',",
+        "    jars = ['Scala/lib/scala-library.jar'],",
+        ")",
+        "",
+        "java_import(",
+        "    name = 'scala',",
+        "    jars = ['Scala/lib/scala-plugin.jar'],",
+        "    runtime_deps = [':scala-library'],",
+        "    visibility = ['//visibility:public'],",
+        ")"]),
+    url = "https://plugins.jetbrains.com/files/1347/33637/scala-intellij-bin-2017.1.15.zip",
 )
 
 # LICENSE: Common Public License 1.0
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
deleted file mode 100644
index d65d9f2..0000000
--- a/aswb/2.2/src/com/google/idea/blaze/android/compatibility/Compatibility.java
+++ /dev/null
@@ -1,138 +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.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/2.2/src/com/google/idea/blaze/android/sync/model/idea/BlazeClassJarProvider.java b/aswb/2.2/src/com/google/idea/blaze/android/sync/model/idea/BlazeClassJarProvider.java
deleted file mode 100644
index 20c2c22..0000000
--- a/aswb/2.2/src/com/google/idea/blaze/android/sync/model/idea/BlazeClassJarProvider.java
+++ /dev/null
@@ -1,40 +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.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.Nullable;
-
-/** Returns no class jars. Used to disable the layout editor loading jars. */
-public class BlazeClassJarProvider extends ClassJarProvider {
-  public BlazeClassJarProvider(Project project) {}
-
-  @Nullable
-  @Override
-  public VirtualFile findModuleClassFile(String className, Module module) {
-    return null;
-  }
-
-  @Override
-  public List<VirtualFile> getModuleExternalLibraries(Module module) {
-    return ImmutableList.of();
-  }
-}
diff --git a/aswb/2.2/tests/utils/integration/com/google/idea/blaze/android/AndroidTestCleanupHelper.java b/aswb/2.2/tests/utils/integration/com/google/idea/blaze/android/AndroidTestCleanupHelper.java
deleted file mode 100644
index 6847c67..0000000
--- a/aswb/2.2/tests/utils/integration/com/google/idea/blaze/android/AndroidTestCleanupHelper.java
+++ /dev/null
@@ -1,61 +0,0 @@
-/*
- * Copyright 2016 The Bazel Authors. All rights reserved.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.google.idea.blaze.android;
-
-import com.intellij.codeInsight.CodeInsightSettings;
-import com.intellij.openapi.project.Project;
-import com.intellij.psi.codeStyle.CodeStyleSchemes;
-import com.intellij.psi.codeStyle.CodeStyleSettings;
-import com.intellij.psi.codeStyle.CodeStyleSettingsManager;
-import com.intellij.psi.impl.source.codeStyle.CodeStyleSchemeImpl;
-import com.intellij.util.xmlb.XmlSerializer;
-import org.jdom.Element;
-
-/**
- * Helper class for cleaning up after Android Studio initialization. Android Studio changes code
- * insight and style settings on initialization, so we need to revert these before tearing down the
- * integration test fixture to avoid failing because of changed settings.<br>
- * TODO: Remove once we build for a version where post-initialization insight and style settings are
- * used in the settings check.
- */
-public final class AndroidTestCleanupHelper {
-
-  public static void cleanUp(Project project) {
-    resetCodeInsightSettings();
-    resetCodeStyleSettings(project);
-  }
-
-  private static void resetCodeInsightSettings() {
-    // We can't just use CodeInsightSettings.getState(), because it excludes fields
-    // matching the default values, and thus wouldn't change anything when loaded.
-    Element codeInsightElement = new Element("state");
-    XmlSerializer.serializeInto(new CodeInsightSettings(), codeInsightElement);
-    CodeInsightSettings.getInstance().loadState(codeInsightElement);
-  }
-
-  private static void resetCodeStyleSettings(Project project) {
-    CodeStyleSettingsManager settingsManager = CodeStyleSettingsManager.getInstance(project);
-    if (settingsManager.USE_PER_PROJECT_SETTINGS && settingsManager.PER_PROJECT_SETTINGS != null) {
-      settingsManager.PER_PROJECT_SETTINGS = new CodeStyleSettings();
-    } else {
-      ((CodeStyleSchemeImpl)
-              CodeStyleSchemes.getInstance()
-                  .findPreferredScheme(settingsManager.PREFERRED_PROJECT_CODE_STYLE))
-          .setCodeStyleSettings(new CodeStyleSettings());
-    }
-  }
-}
diff --git a/aswb/2.2/tests/utils/integration/com/google/idea/blaze/android/AndroidTestSetupRule.java b/aswb/2.2/tests/utils/integration/com/google/idea/blaze/android/AndroidTestSetupRule.java
deleted file mode 100644
index 07fc1ac..0000000
--- a/aswb/2.2/tests/utils/integration/com/google/idea/blaze/android/AndroidTestSetupRule.java
+++ /dev/null
@@ -1,38 +0,0 @@
-/*
- * Copyright 2016 The Bazel Authors. All rights reserved.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.google.idea.blaze.android;
-
-import com.intellij.testFramework.PlatformTestCase;
-import com.intellij.util.PlatformUtils;
-import org.junit.rules.ExternalResource;
-
-/**
- * Runs before Android Studio integration tests, to ensure the AndroidStudio platform prefix is
- * honored.
- */
-public class AndroidTestSetupRule extends ExternalResource {
-
-  @Override
-  protected void before() throws Throwable {
-    // We require idea.platform.prefix to be defined before running tests.
-    // If we don't call this before setting up the test fixture, IntelliJ ignores
-    // the existing value and tries a limited set of candidate prefixes until it finds
-    // a matching descriptor for one of them. Notably, "AndroidStudio" is not a candidate.
-    // The first parameter doesn't matter in our case, so we pass a nonexistent class name.
-    PlatformTestCase.initPlatformPrefix("", System.getProperty(PlatformUtils.PLATFORM_PREFIX_KEY));
-    // TODO: Remove the above once we build for a version where "AndroidStudio" is a candidate.
-  }
-}
diff --git a/aswb/2.2/tests/utils/integration/com/google/idea/blaze/android/BlazeAndroidIntegrationTestCase.java b/aswb/2.2/tests/utils/integration/com/google/idea/blaze/android/BlazeAndroidIntegrationTestCase.java
deleted file mode 100644
index 106860a..0000000
--- a/aswb/2.2/tests/utils/integration/com/google/idea/blaze/android/BlazeAndroidIntegrationTestCase.java
+++ /dev/null
@@ -1,32 +0,0 @@
-/*
- * Copyright 2016 The Bazel Authors. All rights reserved.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.google.idea.blaze.android;
-
-import com.google.idea.blaze.base.BlazeIntegrationTestCase;
-import org.junit.After;
-import org.junit.Rule;
-
-/** Base test class for Blaze Android integration tests. */
-public abstract class BlazeAndroidIntegrationTestCase extends BlazeIntegrationTestCase {
-
-  @Rule public final AndroidTestSetupRule androidSetupRule = new AndroidTestSetupRule();
-
-  @After
-  public final void doTeardown() {
-    AndroidTestCleanupHelper.cleanUp(getProject());
-  }
-}
diff --git a/aswb/2.3/src/META-INF/aswb_beta.xml b/aswb/2.3/src/META-INF/aswb_beta.xml
deleted file mode 100644
index 8df2945..0000000
--- a/aswb/2.3/src/META-INF/aswb_beta.xml
+++ /dev/null
@@ -1,35 +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.
-  -->
-<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
deleted file mode 100644
index e614959..0000000
--- a/aswb/2.3/src/com/google/idea/blaze/android/compatibility/Compatibility.java
+++ /dev/null
@@ -1,141 +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.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/BUILD b/aswb/BUILD
index 16fc99b..7772ebc 100644
--- a/aswb/BUILD
+++ b/aswb/BUILD
@@ -11,7 +11,6 @@
     "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",
@@ -20,14 +19,6 @@
         "//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",
     ],
 )
 
@@ -41,6 +32,7 @@
 
 stamped_plugin_xml(
     name = "stamped_plugin_xml",
+    changelog_file = "//:changelog",
     include_product_code_in_stamp = True,
     plugin_id = "com.google.idea.bazel.aswb",
     plugin_name = "Android Studio with Bazel",
@@ -51,41 +43,26 @@
 
 java_library(
     name = "aswb_lib",
-    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"]),
-    }),
+    srcs = glob(["src/**/*.java"]),
     resources = glob(["resources/**/*"]),
-    visibility = [
-        "//visibility:public",
+    runtime_deps = [
+        "//cpp",
     ],
     deps = [
         "//base",
         "//common/experiments",
-        "//cpp",
         "//intellij_platform_sdk:plugin_api",
         "//java",
-        "//proto_deps",
+        "//proto:proto_deps",
         "@jsr305_annotations//jar",
     ],
 )
 
-# TODO(chaorenl): remove when 2.2 is obsolete
 java_library(
     name = "integration_test_utils",
     testonly = 1,
-    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"]),
-    }),
+    srcs = glob(["tests/utils/integration/**/*.java"]),
     deps = [
-        "//base",
-        "//base:integration_test_utils",
-        "//base:unit_test_utils",
-        "//intellij_platform_sdk:plugin_api_for_tests",
         "@jsr305_annotations//jar",
         "@junit//jar",
     ],
@@ -99,12 +76,7 @@
 
 intellij_unit_test_suite(
     name = "unit_tests",
-    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"]),
-    }),
+    srcs = glob(["tests/unittests/**/*.java"]),
     test_package_root = "com.google.idea.blaze.android",
     deps = [
         ":aswb_lib",
@@ -114,7 +86,7 @@
         "//common/experiments:unit_test_utils",
         "//intellij_platform_sdk:plugin_api_for_tests",
         "//java",
-        "//proto_deps",
+        "//proto:proto_deps",
         "@jsr305_annotations//jar",
         "@junit//jar",
     ],
@@ -122,12 +94,7 @@
 
 intellij_integration_test_suite(
     name = "integration_tests",
-    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"]),
-    }),
+    srcs = glob(["tests/integrationtests/**/*.java"]),
     platform_prefix = "AndroidStudio",
     required_plugins = "com.google.idea.bazel.aswb",
     test_package_root = "com.google.idea.blaze.android",
@@ -142,9 +109,10 @@
         "//base:unit_test_utils",
         "//common/experiments",
         "//common/experiments:unit_test_utils",
+        "//cpp",
         "//intellij_platform_sdk:plugin_api_for_tests",
         "//java",
-        "//proto_deps",
+        "//proto:proto_deps",
         "@jsr305_annotations//jar",
         "@junit//jar",
     ],
diff --git a/aswb/aswb.bazelproject b/aswb/aswb.bazelproject
index c2f7d36..5700471 100644
--- a/aswb/aswb.bazelproject
+++ b/aswb/aswb.bazelproject
@@ -3,9 +3,6 @@
   -ijwb
   -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 d59d935..4bf7584 100644
--- a/aswb/src/META-INF/aswb.xml
+++ b/aswb/src/META-INF/aswb.xml
@@ -18,8 +18,7 @@
 
   <depends>com.intellij.modules.androidstudio</depends>
   <depends>org.jetbrains.android</depends>
-  <!-- TODO(chaorenl): remove when 2.2 is obsolete -->
-  <depends optional="true">com.android.tools.idea.updater</depends>
+  <depends>com.google.gct.test.recorder</depends>
 
   <extensions defaultExtensionNs="com.intellij">
     <java.elementFinder implementation="com.google.idea.blaze.android.resources.AndroidResourceClassFinder"
@@ -40,6 +39,9 @@
     <projectService serviceImplementation="com.google.idea.blaze.android.manifest.ManifestParser"/>
     <projectService serviceImplementation="com.google.idea.blaze.android.sync.model.AndroidResourceModuleRegistry"/>
     <applicationService serviceImplementation="com.google.idea.blaze.android.settings.BlazeAndroidUserSettings"/>
+    <applicationService serviceInterface="com.google.idea.blaze.android.sdk.BlazeSdkProvider"
+      serviceImplementation="com.google.idea.blaze.android.sdk.BlazeSdkProviderImpl"
+      testServiceImplementation="com.google.idea.blaze.android.sdk.MockBlazeSdkProvider"/>
   </extensions>
 
   <extensions defaultExtensionNs="org.jetbrains.android.actions">
@@ -57,12 +59,13 @@
     <SyncListener implementation="com.google.idea.blaze.android.cppimpl.BlazeNdkSupportEnabler"/>
     <SyncListener implementation="com.google.idea.blaze.android.manifest.ManifestParser$ClearManifestParser"/>
     <RunConfigurationFactory implementation="com.google.idea.blaze.android.run.BlazeAndroidRunConfigurationFactory"/>
-    <java.JavaSyncAugmenter implementation="com.google.idea.blaze.android.sync.BlazeAndroidJavaSyncAugmenter"/>
+    <JavaSyncAugmenter implementation="com.google.idea.blaze.android.sync.BlazeAndroidJavaSyncAugmenter"/>
     <PrefetchFileSource implementation="com.google.idea.blaze.android.sync.AndroidPrefetchFileSource"/>
     <BlazeCommandRunConfigurationHandlerProvider implementation="com.google.idea.blaze.android.run.binary.BlazeAndroidBinaryRunConfigurationHandlerProvider"/>
     <BlazeCommandRunConfigurationHandlerProvider implementation="com.google.idea.blaze.android.run.test.BlazeAndroidTestRunConfigurationHandlerProvider"/>
     <BuildSystemAndroidJdkProvider implementation="com.google.idea.blaze.android.sync.BazelAndroidJdkProvider"/>
     <BlazeTestEventsHandler implementation="com.google.idea.blaze.android.run.test.smrunner.BlazeAndroidTestEventsHandler"/>
+    <ProjectViewDefaultValueProvider implementation="com.google.idea.blaze.android.projectview.AndroidSdkPlatformSection$AndroidSdkPlatformProjectViewDefaultValueProvider"/>
   </extensions>
 
   <extensions defaultExtensionNs="com.android.ide">
@@ -84,6 +87,23 @@
   </extensions>
   <!-- END NDK SUPPORT -->
 
+  <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>
+
   <application-components>
     <component>
       <implementation-class>com.google.idea.blaze.android.plugin.PluginCompatibilityEnforcer</implementation-class>
diff --git a/aswb/src/com/google/idea/blaze/android/cppimpl/BlazeNdkSupportEnabler.java b/aswb/src/com/google/idea/blaze/android/cppimpl/BlazeNdkSupportEnabler.java
index f6b50ae..75e1a12 100644
--- a/aswb/src/com/google/idea/blaze/android/cppimpl/BlazeNdkSupportEnabler.java
+++ b/aswb/src/com/google/idea/blaze/android/cppimpl/BlazeNdkSupportEnabler.java
@@ -25,11 +25,10 @@
 import com.google.idea.blaze.base.settings.BlazeImportSettings;
 import com.google.idea.blaze.base.sync.BlazeSyncParams.SyncMode;
 import com.google.idea.blaze.base.sync.SyncListener;
-import com.google.idea.blaze.cpp.BlazeCWorkspace;
 import com.intellij.openapi.application.ApplicationManager;
 import com.intellij.openapi.project.Project;
 import com.jetbrains.cidr.lang.workspace.OCWorkspace;
-import org.jetbrains.annotations.NotNull;
+import com.jetbrains.cidr.lang.workspace.OCWorkspaceManager;
 
 final class BlazeNdkSupportEnabler extends SyncListener.Adapter {
 
@@ -57,7 +56,7 @@
    * @param enabled if true, turn on C support in the IDE. If false, turn off C support in the IDE.
    */
   private static void enableCSupportInIde(Project project, boolean enabled) {
-    BlazeCWorkspace workspace = BlazeCWorkspace.getInstance(project);
+    OCWorkspace workspace = OCWorkspaceManager.getWorkspace(project);
     Boolean isCurrentlyEnabled = !LANGUAGE_SUPPORT_DISABLED.get(project, false);
     if (isCurrentlyEnabled != enabled) {
       NdkHelper.disableCppLanguageSupport(project, !enabled);
@@ -65,7 +64,7 @@
     }
   }
 
-  private static void rebuildSymbols(@NotNull Project project, @NotNull OCWorkspace workspace) {
+  private static void rebuildSymbols(Project project, OCWorkspace workspace) {
     ApplicationManager.getApplication()
         .runReadAction(
             () -> {
diff --git a/aswb/2.3/src/com/google/idea/blaze/android/project/BlazeBuildSystemService.java b/aswb/src/com/google/idea/blaze/android/project/BlazeBuildSystemService.java
similarity index 100%
rename from aswb/2.3/src/com/google/idea/blaze/android/project/BlazeBuildSystemService.java
rename to aswb/src/com/google/idea/blaze/android/project/BlazeBuildSystemService.java
diff --git a/aswb/2.3/src/com/google/idea/blaze/android/project/BlazeFeatureEnableService.java b/aswb/src/com/google/idea/blaze/android/project/BlazeFeatureEnableService.java
similarity index 73%
rename from aswb/2.3/src/com/google/idea/blaze/android/project/BlazeFeatureEnableService.java
rename to aswb/src/com/google/idea/blaze/android/project/BlazeFeatureEnableService.java
index e59f375..a43374f 100644
--- a/aswb/2.3/src/com/google/idea/blaze/android/project/BlazeFeatureEnableService.java
+++ b/aswb/src/com/google/idea/blaze/android/project/BlazeFeatureEnableService.java
@@ -16,7 +16,9 @@
 package com.google.idea.blaze.android.project;
 
 import com.android.tools.idea.project.FeatureEnableService;
+import com.google.common.collect.ImmutableMap;
 import com.google.idea.blaze.android.settings.BlazeAndroidUserSettings;
+import com.google.idea.blaze.base.logging.EventLogger;
 import com.google.idea.blaze.base.settings.Blaze;
 import com.google.idea.blaze.base.sync.data.BlazeProjectDataManager;
 import com.google.idea.common.experiments.BoolExperiment;
@@ -24,6 +26,8 @@
 
 /** Enable features supported by the blaze integration. */
 public class BlazeFeatureEnableService extends FeatureEnableService {
+  private static final EventLogger logger = EventLogger.getInstance();
+
   private static final BoolExperiment ENABLE_LAYOUT_EDITOR =
       new BoolExperiment("enable.layout.editor", true);
 
@@ -34,10 +38,13 @@
 
   @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;
+    boolean isEnabled =
+        isLayoutEditorExperimentEnabled()
+            && BlazeAndroidUserSettings.getInstance().getUseLayoutEditor();
+    boolean isReady = BlazeProjectDataManager.getInstance(project).getBlazeProjectData() != null;
+    logger.log(
+        getClass(), "layout_editor", ImmutableMap.of("enabled", Boolean.toString(isEnabled)));
+    return isEnabled && isReady;
   }
 
   public static boolean isLayoutEditorExperimentEnabled() {
diff --git a/aswb/src/com/google/idea/blaze/android/projectview/AndroidSdkPlatformSection.java b/aswb/src/com/google/idea/blaze/android/projectview/AndroidSdkPlatformSection.java
index fe75735..917da87 100644
--- a/aswb/src/com/google/idea/blaze/android/projectview/AndroidSdkPlatformSection.java
+++ b/aswb/src/com/google/idea/blaze/android/projectview/AndroidSdkPlatformSection.java
@@ -18,17 +18,20 @@
 import static java.util.stream.Collectors.toList;
 
 import com.google.common.collect.ImmutableList;
-import com.google.idea.blaze.android.compatibility.Compatibility.AndroidSdkUtils;
+import com.google.idea.blaze.android.sdk.BlazeSdkProvider;
 import com.google.idea.blaze.android.sync.sdk.AndroidSdkFromProjectView;
 import com.google.idea.blaze.base.projectview.ProjectView;
+import com.google.idea.blaze.base.projectview.ProjectViewSet;
 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.ProjectViewDefaultValueProvider;
 import com.google.idea.blaze.base.projectview.section.ScalarSection;
 import com.google.idea.blaze.base.projectview.section.ScalarSectionParser;
 import com.google.idea.blaze.base.projectview.section.SectionKey;
 import com.google.idea.blaze.base.projectview.section.SectionParser;
 import com.google.idea.blaze.base.projectview.section.sections.TextBlock;
 import com.google.idea.blaze.base.projectview.section.sections.TextBlockSection;
+import com.google.idea.blaze.base.settings.Blaze.BuildSystem;
 import com.intellij.openapi.projectRoots.Sdk;
 import com.intellij.openapi.util.text.StringUtil;
 import java.util.List;
@@ -60,15 +63,19 @@
     public ItemType getItemType() {
       return ItemType.Other;
     }
+  }
 
+  static class AndroidSdkPlatformProjectViewDefaultValueProvider
+      implements ProjectViewDefaultValueProvider {
     @Override
-    public ProjectView addProjectViewDefaultValue(ProjectView projectView) {
-      if (!projectView.getSectionsOfType(KEY).isEmpty()) {
-        return projectView;
+    public ProjectView addProjectViewDefaultValue(
+        BuildSystem buildSystem, ProjectViewSet projectViewSet, ProjectView topLevelProjectView) {
+      if (!topLevelProjectView.getSectionsOfType(KEY).isEmpty()) {
+        return topLevelProjectView;
       }
-      List<Sdk> sdks = AndroidSdkUtils.getAllAndroidSdks();
+      List<Sdk> sdks = BlazeSdkProvider.getInstance().getAllAndroidSdks();
       ProjectView.Builder builder =
-          ProjectView.builder(projectView).add(TextBlockSection.of(TextBlock.newLine()));
+          ProjectView.builder(topLevelProjectView).add(TextBlockSection.of(TextBlock.newLine()));
 
       if (sdks.isEmpty()) {
         builder
@@ -81,7 +88,7 @@
       } else if (sdks.size() == 1) {
         builder.add(
             ScalarSection.builder(KEY)
-                .set(AndroidSdkFromProjectView.getSdkTargetHash(sdks.get(0))));
+                .set(BlazeSdkProvider.getInstance().getSdkTargetHash(sdks.get(0))));
       } else {
         builder.add(
             TextBlockSection.of(
@@ -95,5 +102,10 @@
       }
       return builder.build();
     }
+
+    @Override
+    public SectionKey<?, ?> getSectionKey() {
+      return KEY;
+    }
   }
 }
diff --git a/aswb/2.3/src/com/google/idea/blaze/android/rendering/BlazeRenderErrorContributor.java b/aswb/src/com/google/idea/blaze/android/rendering/BlazeRenderErrorContributor.java
similarity index 100%
rename from aswb/2.3/src/com/google/idea/blaze/android/rendering/BlazeRenderErrorContributor.java
rename to aswb/src/com/google/idea/blaze/android/rendering/BlazeRenderErrorContributor.java
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 6926023..eaff918 100644
--- a/aswb/src/com/google/idea/blaze/android/run/BlazeAndroidRunConfigurationCommonState.java
+++ b/aswb/src/com/google/idea/blaze/android/run/BlazeAndroidRunConfigurationCommonState.java
@@ -18,7 +18,6 @@
 import static com.google.idea.blaze.android.cppapi.NdkSupport.NDK_SUPPORT;
 
 import com.android.tools.idea.run.ValidationError;
-import com.android.tools.idea.run.editor.AndroidDebugger;
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.Lists;
 import com.google.idea.blaze.android.cppapi.NdkSupport;
@@ -76,12 +75,8 @@
     return debuggerManager;
   }
 
-  public List<String> getUserFlags() {
-    return userFlags.getFlags();
-  }
-
-  public void setUserFlags(List<String> userFlags) {
-    this.userFlags.setFlags(userFlags);
+  public RunConfigurationFlagsState getBlazeFlagsState() {
+    return userFlags;
   }
 
   public boolean isNativeDebuggingEnabled() {
@@ -92,10 +87,11 @@
     this.nativeDebuggingEnabled = nativeDebuggingEnabled;
   }
 
-  public ImmutableList<String> getBuildFlags(Project project, ProjectViewSet projectViewSet) {
+  public ImmutableList<String> getExpandedBuildFlags(
+      Project project, ProjectViewSet projectViewSet) {
     return ImmutableList.<String>builder()
         .addAll(BlazeFlags.buildFlags(project, projectViewSet))
-        .addAll(getUserFlags())
+        .addAll(getBlazeFlagsState().getExpandedFlags())
         .addAll(getNativeDebuggerFlags())
         .build();
   }
@@ -127,19 +123,11 @@
     Element deployTargetStatesElement = element.getChild(DEPLOY_TARGET_STATES_TAG);
     if (deployTargetStatesElement != null) {
       deployTargetManager.readExternal(deployTargetStatesElement);
-    } else {
-      // TODO Introduced in 1.12, remove in 1.14.
-      // This was for migrating the state to a child element.
-      deployTargetManager.readExternal(element);
     }
 
     Element debuggerStatesElement = element.getChild(DEBUGGER_STATES_TAG);
     if (debuggerStatesElement != null) {
       debuggerManager.readExternal(debuggerStatesElement);
-    } else {
-      // TODO Introduced in 1.12, remove in 1.14.
-      // This was for migrating the state to a child element.
-      debuggerManager.readExternal(element);
     }
   }
 
@@ -148,8 +136,6 @@
     userFlags.writeExternal(element);
     element.setAttribute(NATIVE_DEBUG_ATTR, Boolean.toString(nativeDebuggingEnabled));
 
-    removeOldManagerState(element);
-
     element.removeChildren(DEPLOY_TARGET_STATES_TAG);
     Element deployTargetStatesElement = new Element(DEPLOY_TARGET_STATES_TAG);
     deployTargetManager.writeExternal(deployTargetStatesElement);
@@ -161,19 +147,6 @@
     element.addContent(debuggerStatesElement);
   }
 
-  // TODO Introduced in 1.12, remove in 1.14. This was for migrating state
-  // and cleaning up mass amounts of duplicate state caused by never removing these elements before.
-  private void removeOldManagerState(Element element) {
-    // This is safe because we know only BlazeAndroidRunConfigurationDeployTargetManager
-    // directly wrote option elements (via DefaultJDOMExternalizer.writeExternal) to our root.
-    element.removeChildren("option");
-    // BlazeAndroidRunConfigurationDebuggerManager, meanwhile, nested its state in
-    // child elements named after the AndroidDebugger extension IDs.
-    for (AndroidDebugger<?> debugger : AndroidDebugger.EP_NAME.getExtensions()) {
-      element.removeChildren(debugger.getId());
-    }
-  }
-
   @Override
   public RunConfigurationStateEditor getEditor(Project project) {
     return new BlazeAndroidRunConfigurationCommonStateEditor(this, project);
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 5351213..cb6b23c 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,7 +33,6 @@
 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;
@@ -175,8 +174,7 @@
       Set<String> packageIds,
       boolean monitorRemoteProcess)
       throws ExecutionException {
-    return Compatibility.getConnectDebuggerTask(
-        androidDebugger,
+    return androidDebugger.getConnectDebuggerTask(
         env,
         null,
         packageIds,
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 3b222f7..f57d51f 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
@@ -104,7 +104,8 @@
     ProjectViewSet projectViewSet = ProjectViewManager.getInstance(project).getProjectViewSet();
     BlazeAndroidRunConfigurationValidationUtil.validateExecution(module, facet, projectViewSet);
 
-    ImmutableList<String> buildFlags = configState.getBuildFlags(project, projectViewSet);
+    ImmutableList<String> buildFlags =
+        configState.getCommonState().getExpandedBuildFlags(project, projectViewSet);
     BlazeAndroidRunContext runContext = createRunContext(project, facet, environment, buildFlags);
 
     return new BlazeAndroidRunConfigurationRunner(
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 cb368d4..8bc2076 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
@@ -18,10 +18,8 @@
 import com.android.tools.idea.run.ValidationError;
 import com.android.tools.idea.run.util.LaunchUtils;
 import com.google.common.base.Strings;
-import com.google.common.collect.ImmutableList;
 import com.google.common.collect.Maps;
 import com.google.idea.blaze.android.run.BlazeAndroidRunConfigurationCommonState;
-import com.google.idea.blaze.base.projectview.ProjectViewSet;
 import com.google.idea.blaze.base.run.state.RunConfigurationState;
 import com.google.idea.blaze.base.run.state.RunConfigurationStateEditor;
 import com.intellij.openapi.project.Project;
@@ -123,10 +121,6 @@
     this.mode = mode;
   }
 
-  public ImmutableList<String> getBuildFlags(Project project, ProjectViewSet projectViewSet) {
-    return commonState.getBuildFlags(project, projectViewSet);
-  }
-
   /**
    * We collect errors rather than throwing to avoid missing fatal errors by exiting early for a
    * warning.
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 2ea7ebc..da33631 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,7 +30,6 @@
 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;
@@ -167,8 +166,7 @@
       Set<String> packageIds,
       boolean monitorRemoteProcess)
       throws ExecutionException {
-    return Compatibility.getConnectDebuggerTask(
-        androidDebugger,
+    return androidDebugger.getConnectDebuggerTask(
         env,
         null,
         packageIds,
diff --git a/aswb/src/com/google/idea/blaze/android/run/binary/mobileinstall/BlazeApkBuildStepMobileInstall.java b/aswb/src/com/google/idea/blaze/android/run/binary/mobileinstall/BlazeApkBuildStepMobileInstall.java
index 1256ff4..582afea 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,22 +23,18 @@
 import com.google.common.util.concurrent.ListenableFuture;
 import com.google.common.util.concurrent.SettableFuture;
 import com.google.common.util.concurrent.UncheckedExecutionException;
-import com.google.idea.blaze.android.compatibility.Compatibility.AndroidSdkUtils;
 import com.google.idea.blaze.android.run.deployinfo.BlazeAndroidDeployInfo;
 import com.google.idea.blaze.android.run.deployinfo.BlazeApkDeployInfoProtoHelper;
 import com.google.idea.blaze.android.run.runner.BlazeAndroidDeviceSelector;
 import com.google.idea.blaze.android.run.runner.BlazeApkBuildStep;
-import com.google.idea.blaze.android.sync.model.AndroidSdkPlatform;
-import com.google.idea.blaze.android.sync.model.BlazeAndroidSyncData;
 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.buildresult.BuildResultHelper;
 import com.google.idea.blaze.base.filecache.FileCaches;
 import com.google.idea.blaze.base.issueparser.IssueOutputLineProcessor;
-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.WorkspaceRoot;
 import com.google.idea.blaze.base.scope.BlazeContext;
@@ -46,17 +42,15 @@
 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.Blaze;
-import com.google.idea.blaze.base.sync.data.BlazeProjectDataManager;
 import com.google.idea.blaze.base.util.SaveUtil;
 import com.google.idea.common.experiments.BoolExperiment;
 import com.intellij.execution.ExecutionException;
 import com.intellij.execution.runners.ExecutionEnvironment;
 import com.intellij.openapi.project.Project;
-import com.intellij.openapi.projectRoots.Sdk;
 import java.io.File;
-import java.nio.file.Paths;
 import java.util.concurrent.CancellationException;
 import javax.annotation.Nullable;
+import org.jetbrains.android.sdk.AndroidSdkUtils;
 
 /** Builds and installs the APK using mobile-install. */
 public class BlazeApkBuildStepMobileInstall implements BlazeApkBuildStep {
@@ -99,11 +93,12 @@
             }
             BlazeCommand.Builder command =
                 BlazeCommand.builder(
-                    Blaze.getBuildSystem(project), BlazeCommandName.MOBILE_INSTALL);
+                    Blaze.getBuildSystemProvider(project).getBinaryPath(),
+                    BlazeCommandName.MOBILE_INSTALL);
             command.addBlazeFlags(BlazeFlags.adbSerialFlags(device.getSerialNumber()));
 
             if (USE_SDK_ADB.getValue()) {
-              File adb = getSdkAdb(project);
+              File adb = AndroidSdkUtils.getAdb(project);
               if (adb != null) {
                 command.addBlazeFlags(ImmutableList.of("--adb", adb.toString()));
               }
@@ -117,13 +112,14 @@
             }
             WorkspaceRoot workspaceRoot = WorkspaceRoot.fromProject(project);
 
+            BlazeApkDeployInfoProtoHelper deployInfoHelper =
+                new BlazeApkDeployInfoProtoHelper(project, buildFlags);
+            BuildResultHelper buildResultHelper = deployInfoHelper.getBuildResultHelper();
+
             command
                 .addTargets(label)
                 .addBlazeFlags(buildFlags)
-                .addBlazeFlags(BlazeFlags.EXPERIMENTAL_SHOW_ARTIFACTS);
-
-            BlazeApkDeployInfoProtoHelper deployInfoHelper =
-                new BlazeApkDeployInfoProtoHelper(project, buildFlags);
+                .addBlazeFlags(buildResultHelper.getBuildFlags());
 
             SaveUtil.saveAllFiles();
             int retVal =
@@ -131,8 +127,7 @@
                     .addBlazeCommand(command.build())
                     .context(context)
                     .stderr(
-                        LineProcessingOutputStream.of(
-                            deployInfoHelper.getLineProcessor(),
+                        buildResultHelper.stderr(
                             new IssueOutputLineProcessor(project, context, workspaceRoot)))
                     .build()
                     .run();
@@ -172,35 +167,6 @@
     return deployInfoFuture;
   }
 
-  private static File getSdkAdb(Project project) {
-    BlazeProjectData projectData =
-        BlazeProjectDataManager.getInstance(project).getBlazeProjectData();
-    if (projectData == null) {
-      return null;
-    }
-    BlazeAndroidSyncData syncData = projectData.syncState.get(BlazeAndroidSyncData.class);
-    if (syncData == null) {
-      return null;
-    }
-    AndroidSdkPlatform androidSdkPlatform = syncData.androidSdkPlatform;
-    if (androidSdkPlatform == null) {
-      return null;
-    }
-    Sdk sdk = AndroidSdkUtils.findSuitableAndroidSdk(androidSdkPlatform.androidSdk);
-    if (sdk == null) {
-      return null;
-    }
-    String homePath = sdk.getHomePath();
-    if (homePath == null) {
-      return null;
-    }
-    File adb = Paths.get(homePath, "platform-tools", "adb").toFile();
-    if (!adb.exists()) {
-      return null;
-    }
-    return adb;
-  }
-
   @Nullable
   private static IDevice resolveDevice(BlazeContext context, DeviceFutures deviceFutures) {
     if (deviceFutures.get().size() != 1) {
diff --git a/aswb/src/com/google/idea/blaze/android/run/deployinfo/BlazeApkDeployInfoProtoHelper.java b/aswb/src/com/google/idea/blaze/android/run/deployinfo/BlazeApkDeployInfoProtoHelper.java
index 241fd76..98e2e87 100644
--- a/aswb/src/com/google/idea/blaze/android/run/deployinfo/BlazeApkDeployInfoProtoHelper.java
+++ b/aswb/src/com/google/idea/blaze/android/run/deployinfo/BlazeApkDeployInfoProtoHelper.java
@@ -17,12 +17,11 @@
 
 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.idea.blaze.android.manifest.ManifestParser;
-import com.google.idea.blaze.base.async.process.LineProcessingOutputStream;
-import com.google.idea.blaze.base.command.ExperimentalShowArtifactsLineProcessor;
+import com.google.idea.blaze.base.command.buildresult.BuildResultHelper;
 import com.google.idea.blaze.base.command.info.BlazeInfo;
+import com.google.idea.blaze.base.command.info.BlazeInfoRunner;
 import com.google.idea.blaze.base.model.primitives.WorkspaceRoot;
 import com.google.idea.blaze.base.scope.BlazeContext;
 import com.google.idea.blaze.base.settings.Blaze;
@@ -44,24 +43,23 @@
   private final Project project;
   private final WorkspaceRoot workspaceRoot;
   private final ImmutableList<String> buildFlags;
-  private final List<File> deployInfoFiles = Lists.newArrayList();
-  private final LineProcessingOutputStream.LineProcessor lineProcessor =
-      new ExperimentalShowArtifactsLineProcessor(
-          deployInfoFiles, fileName -> fileName.endsWith(".deployinfo.pb"));
+  private final BuildResultHelper buildResultHelper;
 
   public BlazeApkDeployInfoProtoHelper(Project project, ImmutableList<String> buildFlags) {
     this.project = project;
     this.buildFlags = buildFlags;
     this.workspaceRoot = WorkspaceRoot.fromProject(project);
+    this.buildResultHelper =
+        BuildResultHelper.forFiles(fileName -> fileName.endsWith(".deployinfo.pb"));
   }
 
-  public LineProcessingOutputStream.LineProcessor getLineProcessor() {
-    return lineProcessor;
+  public BuildResultHelper getBuildResultHelper() {
+    return buildResultHelper;
   }
 
   @Nullable
   public BlazeAndroidDeployInfo readDeployInfo(BlazeContext context) {
-    File deployInfoFile = Iterables.getOnlyElement(deployInfoFiles, null);
+    File deployInfoFile = Iterables.getOnlyElement(buildResultHelper.getBuildArtifacts(), null);
     if (deployInfoFile == null) {
       return null;
     }
@@ -88,10 +86,10 @@
   @Nullable
   private String getExecutionRoot(BlazeContext context) {
     ListenableFuture<String> execRootFuture =
-        BlazeInfo.getInstance()
+        BlazeInfoRunner.getInstance()
             .runBlazeInfo(
                 context,
-                Blaze.getBuildSystem(project),
+                Blaze.getBuildSystemProvider(project).getBinaryPath(),
                 workspaceRoot,
                 buildFlags,
                 BlazeInfo.EXECUTION_ROOT_KEY);
diff --git a/aswb/src/com/google/idea/blaze/android/run/runner/BlazeApkBuildStepNormalBuild.java b/aswb/src/com/google/idea/blaze/android/run/runner/BlazeApkBuildStepNormalBuild.java
index 835aa1d..58daf4f 100644
--- a/aswb/src/com/google/idea/blaze/android/run/runner/BlazeApkBuildStepNormalBuild.java
+++ b/aswb/src/com/google/idea/blaze/android/run/runner/BlazeApkBuildStepNormalBuild.java
@@ -23,10 +23,9 @@
 import com.google.idea.blaze.android.run.deployinfo.BlazeApkDeployInfoProtoHelper;
 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.buildresult.BuildResultHelper;
 import com.google.idea.blaze.base.filecache.FileCaches;
 import com.google.idea.blaze.base.issueparser.IssueOutputLineProcessor;
 import com.google.idea.blaze.base.model.primitives.Label;
@@ -63,17 +62,19 @@
           @Override
           protected void execute(@NotNull BlazeContext context) {
             BlazeCommand.Builder command =
-                BlazeCommand.builder(Blaze.getBuildSystem(project), BlazeCommandName.BUILD);
+                BlazeCommand.builder(
+                    Blaze.getBuildSystemProvider(project).getBinaryPath(), BlazeCommandName.BUILD);
             WorkspaceRoot workspaceRoot = WorkspaceRoot.fromProject(project);
 
+            BlazeApkDeployInfoProtoHelper deployInfoHelper =
+                new BlazeApkDeployInfoProtoHelper(project, buildFlags);
+            BuildResultHelper buildResultHelper = deployInfoHelper.getBuildResultHelper();
+
             command
                 .addTargets(label)
                 .addBlazeFlags("--output_groups=+android_deploy_info")
                 .addBlazeFlags(buildFlags)
-                .addBlazeFlags(BlazeFlags.EXPERIMENTAL_SHOW_ARTIFACTS);
-
-            BlazeApkDeployInfoProtoHelper deployInfoHelper =
-                new BlazeApkDeployInfoProtoHelper(project, buildFlags);
+                .addBlazeFlags(buildResultHelper.getBuildFlags());
 
             SaveUtil.saveAllFiles();
             int retVal =
@@ -81,8 +82,7 @@
                     .addBlazeCommand(command.build())
                     .context(context)
                     .stderr(
-                        LineProcessingOutputStream.of(
-                            deployInfoHelper.getLineProcessor(),
+                        buildResultHelper.stderr(
                             new IssueOutputLineProcessor(project, context, workspaceRoot)))
                     .build()
                     .run();
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 c0cbb24..92311c4 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,7 +16,7 @@
 package com.google.idea.blaze.android.run.test;
 
 import com.android.tools.idea.run.ConsoleProvider;
-import com.google.idea.blaze.android.compatibility.Compatibility.AndroidTestConsoleProperties;
+import com.android.tools.idea.testartifacts.instrumented.AndroidTestConsoleProperties;
 import com.google.idea.blaze.base.run.smrunner.BlazeTestEventsHandler;
 import com.google.idea.blaze.base.run.smrunner.SmRunnerUtils;
 import com.intellij.execution.ExecutionException;
diff --git a/aswb/src/com/google/idea/blaze/android/run/test/BlazeAndroidTestClassRunConfigurationProducer.java b/aswb/src/com/google/idea/blaze/android/run/test/BlazeAndroidTestClassRunConfigurationProducer.java
index a9b9515..b898e42 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.testartifacts.instrumented.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;
@@ -75,7 +75,7 @@
     }
     sourceElement.set(testClass);
 
-    TargetIdeInfo target = RunUtil.targetForTestClass(context.getProject(), testClass, null);
+    TargetIdeInfo target = RunUtil.targetForTestClass(testClass, null);
     if (target == null) {
       return false;
     }
diff --git a/aswb/src/com/google/idea/blaze/android/run/test/BlazeAndroidTestFilter.java b/aswb/src/com/google/idea/blaze/android/run/test/BlazeAndroidTestFilter.java
index e5f2b1d..9002f30 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.testartifacts.instrumented.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/BlazeAndroidTestLaunchTask.java b/aswb/src/com/google/idea/blaze/android/run/test/BlazeAndroidTestLaunchTask.java
index 7283e53..5bd9892 100644
--- a/aswb/src/com/google/idea/blaze/android/run/test/BlazeAndroidTestLaunchTask.java
+++ b/aswb/src/com/google/idea/blaze/android/run/test/BlazeAndroidTestLaunchTask.java
@@ -132,7 +132,8 @@
 
                         BlazeCommand.Builder commandBuilder =
                             BlazeCommand.builder(
-                                    Blaze.getBuildSystem(project), BlazeCommandName.TEST)
+                                    Blaze.getBuildSystemProvider(project).getBinaryPath(),
+                                    BlazeCommandName.TEST)
                                 .addTargets(target);
                         // Build flags must match BlazeBeforeRunTask.
                         commandBuilder.addBlazeFlags(buildFlags);
diff --git a/aswb/src/com/google/idea/blaze/android/run/test/BlazeAndroidTestMethodRunConfigurationProducer.java b/aswb/src/com/google/idea/blaze/android/run/test/BlazeAndroidTestMethodRunConfigurationProducer.java
index a0d4878..db2cca6 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.testartifacts.instrumented.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;
@@ -74,7 +74,7 @@
       return false;
     }
 
-    TargetIdeInfo target = RunUtil.targetForTestClass(context.getProject(), containingClass, null);
+    TargetIdeInfo target = RunUtil.targetForTestClass(containingClass, null);
     if (target == null) {
       return false;
     }
diff --git a/aswb/src/com/google/idea/blaze/android/run/test/BlazeAndroidTestRunConfigurationHandler.java b/aswb/src/com/google/idea/blaze/android/run/test/BlazeAndroidTestRunConfigurationHandler.java
index d63ea0c..4b94b3c 100644
--- a/aswb/src/com/google/idea/blaze/android/run/test/BlazeAndroidTestRunConfigurationHandler.java
+++ b/aswb/src/com/google/idea/blaze/android/run/test/BlazeAndroidTestRunConfigurationHandler.java
@@ -102,7 +102,8 @@
     ProjectViewSet projectViewSet = ProjectViewManager.getInstance(project).getProjectViewSet();
     BlazeAndroidRunConfigurationValidationUtil.validateExecution(module, facet, projectViewSet);
 
-    ImmutableList<String> buildFlags = configState.getBuildFlags(project, projectViewSet);
+    ImmutableList<String> buildFlags =
+        configState.getCommonState().getExpandedBuildFlags(project, projectViewSet);
     BlazeAndroidRunContext runContext = createRunContext(project, facet, environment, buildFlags);
 
     return new BlazeAndroidRunConfigurationRunner(
diff --git a/aswb/src/com/google/idea/blaze/android/run/test/BlazeAndroidTestRunConfigurationState.java b/aswb/src/com/google/idea/blaze/android/run/test/BlazeAndroidTestRunConfigurationState.java
index 62fcbb3..074b17b 100644
--- a/aswb/src/com/google/idea/blaze/android/run/test/BlazeAndroidTestRunConfigurationState.java
+++ b/aswb/src/com/google/idea/blaze/android/run/test/BlazeAndroidTestRunConfigurationState.java
@@ -17,10 +17,8 @@
 
 import com.android.tools.idea.run.ValidationError;
 import com.google.common.base.Strings;
-import com.google.common.collect.ImmutableList;
 import com.google.common.collect.Maps;
 import com.google.idea.blaze.android.run.BlazeAndroidRunConfigurationCommonState;
-import com.google.idea.blaze.base.projectview.ProjectViewSet;
 import com.google.idea.blaze.base.run.state.RunConfigurationState;
 import com.google.idea.blaze.base.run.state.RunConfigurationStateEditor;
 import com.intellij.openapi.project.Project;
@@ -132,10 +130,6 @@
     this.extraOptions = extraOptions;
   }
 
-  public ImmutableList<String> getBuildFlags(Project project, ProjectViewSet projectViewSet) {
-    return commonState.getBuildFlags(project, projectViewSet);
-  }
-
   /**
    * We collect errors rather than throwing to avoid missing fatal errors by exiting early for a
    * warning.
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 5df4512..5e5e6c8 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.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 static com.android.tools.idea.testartifacts.instrumented.AndroidTestRunConfiguration.TEST_ALL_IN_MODULE;
+import static com.android.tools.idea.testartifacts.instrumented.AndroidTestRunConfiguration.TEST_ALL_IN_PACKAGE;
+import static com.android.tools.idea.testartifacts.instrumented.AndroidTestRunConfiguration.TEST_CLASS;
+import static com.android.tools.idea.testartifacts.instrumented.AndroidTestRunConfiguration.TEST_METHOD;
 
 import com.google.idea.blaze.base.run.state.RunConfigurationState;
 import com.google.idea.blaze.base.run.state.RunConfigurationStateEditor;
diff --git a/aswb/src/com/google/idea/blaze/android/run/test/BlazeAndroidTestRunContext.java b/aswb/src/com/google/idea/blaze/android/run/test/BlazeAndroidTestRunContext.java
index 7f5fb95..9962acf 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,7 +33,6 @@
 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;
@@ -45,7 +44,6 @@
 import com.google.idea.blaze.base.model.primitives.Label;
 import com.google.idea.blaze.base.run.BlazeCommandRunConfiguration;
 import com.google.idea.blaze.base.run.smrunner.BlazeTestEventsHandler;
-import com.google.idea.common.experiments.BoolExperiment;
 import com.intellij.execution.ExecutionException;
 import com.intellij.execution.Executor;
 import com.intellij.execution.executors.DefaultDebugExecutor;
@@ -61,9 +59,6 @@
 /** Run context for android_test. */
 class BlazeAndroidTestRunContext implements BlazeAndroidRunContext {
 
-  private static final BoolExperiment smRunnerUiEnabled =
-      new BoolExperiment("use.smrunner.ui.android", true);
-
   private final Project project;
   private final AndroidFacet facet;
   private final BlazeCommandRunConfiguration runConfiguration;
@@ -97,7 +92,7 @@
     this.apkProvider = new BlazeApkProvider(project, buildStep.getDeployInfo());
 
     BlazeTestEventsHandler testEventsHandler = null;
-    if (smRunnerUiEnabled.getValue() && !isDebugging(env.getExecutor())) {
+    if (!isDebugging(env.getExecutor())) {
       testEventsHandler =
           BlazeTestEventsHandler.getHandlerForTarget(project, runConfiguration.getTarget());
       assert (testEventsHandler != null);
@@ -230,8 +225,7 @@
       return new ConnectBlazeTestDebuggerTask(
           env.getProject(), androidDebugger, packageIds, applicationIdProvider, this);
     }
-    return Compatibility.getConnectDebuggerTask(
-        androidDebugger,
+    return androidDebugger.getConnectDebuggerTask(
         env,
         null,
         packageIds,
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 8e2328e..8cdeedf 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;
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 2430f3c..a10773e 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
@@ -22,8 +22,8 @@
 import com.android.tools.idea.run.ConsolePrinter;
 import com.android.tools.idea.run.tasks.LaunchTask;
 import com.android.tools.idea.run.util.LaunchStatus;
+import com.android.tools.idea.testartifacts.instrumented.AndroidTestListener;
 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;
@@ -158,6 +158,7 @@
   }
 
   @Override
+  @SuppressWarnings("FutureReturnValueIgnored")
   public boolean perform(
       IDevice device, final LaunchStatus launchStatus, final ConsolePrinter printer) {
     printer.stdout("Running tests\n");
@@ -165,6 +166,8 @@
     final RemoteAndroidTestRunner runner =
         new RemoteAndroidTestRunner(testApplicationId, instrumentationTestRunner, device);
     switch (configState.getTestingType()) {
+      case BlazeAndroidTestRunConfigurationState.TEST_ALL_IN_MODULE:
+        break;
       case BlazeAndroidTestRunConfigurationState.TEST_ALL_IN_PACKAGE:
         runner.setTestPackageName(configState.getPackageName());
         break;
@@ -183,15 +186,12 @@
     // run in a separate thread as this will block until the tests complete
     ApplicationManager.getApplication()
         .executeOnPooledThread(
-            new Runnable() {
-              @Override
-              public void run() {
-                try {
-                  runner.run(new AndroidTestListener(launchStatus, printer));
-                } catch (Exception e) {
-                  LOG.info(e);
-                  printer.stderr("Error: Unexpected exception while running tests: " + e);
-                }
+            () -> {
+              try {
+                runner.run(new AndroidTestListener(launchStatus, printer));
+              } catch (Exception e) {
+                LOG.info(e);
+                printer.stderr("Error: Unexpected exception while running tests: " + e);
               }
             });
 
diff --git a/aswb/2.3/src/com/google/idea/blaze/android/run/testrecorder/TestRecorderBlazeCommandRunConfiguration.java b/aswb/src/com/google/idea/blaze/android/run/testrecorder/TestRecorderBlazeCommandRunConfiguration.java
similarity index 100%
rename from aswb/2.3/src/com/google/idea/blaze/android/run/testrecorder/TestRecorderBlazeCommandRunConfiguration.java
rename to aswb/src/com/google/idea/blaze/android/run/testrecorder/TestRecorderBlazeCommandRunConfiguration.java
diff --git a/aswb/2.3/src/com/google/idea/blaze/android/run/testrecorder/TestRecorderBlazeCommandRunConfigurationProxy.java b/aswb/src/com/google/idea/blaze/android/run/testrecorder/TestRecorderBlazeCommandRunConfigurationProxy.java
similarity index 100%
rename from aswb/2.3/src/com/google/idea/blaze/android/run/testrecorder/TestRecorderBlazeCommandRunConfigurationProxy.java
rename to aswb/src/com/google/idea/blaze/android/run/testrecorder/TestRecorderBlazeCommandRunConfigurationProxy.java
diff --git a/aswb/2.3/src/com/google/idea/blaze/android/run/testrecorder/TestRecorderBlazeCommandRunConfigurationProxyProvider.java b/aswb/src/com/google/idea/blaze/android/run/testrecorder/TestRecorderBlazeCommandRunConfigurationProxyProvider.java
similarity index 100%
rename from aswb/2.3/src/com/google/idea/blaze/android/run/testrecorder/TestRecorderBlazeCommandRunConfigurationProxyProvider.java
rename to aswb/src/com/google/idea/blaze/android/run/testrecorder/TestRecorderBlazeCommandRunConfigurationProxyProvider.java
diff --git a/aswb/src/com/google/idea/blaze/android/sdk/BlazeSdkProvider.java b/aswb/src/com/google/idea/blaze/android/sdk/BlazeSdkProvider.java
new file mode 100644
index 0000000..2222bd4
--- /dev/null
+++ b/aswb/src/com/google/idea/blaze/android/sdk/BlazeSdkProvider.java
@@ -0,0 +1,35 @@
+/*
+ * Copyright 2017 The Bazel Authors. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.idea.blaze.android.sdk;
+
+import com.intellij.openapi.components.ServiceManager;
+import com.intellij.openapi.projectRoots.Sdk;
+import java.util.List;
+import javax.annotation.Nullable;
+
+/** Indirection to Sdks for testing purposes. */
+public interface BlazeSdkProvider {
+  static BlazeSdkProvider getInstance() {
+    return ServiceManager.getService(BlazeSdkProvider.class);
+  }
+
+  List<Sdk> getAllAndroidSdks();
+
+  Sdk findSdk(String targetHash);
+
+  @Nullable
+  String getSdkTargetHash(Sdk sdk);
+}
diff --git a/aswb/src/com/google/idea/blaze/android/sdk/BlazeSdkProviderImpl.java b/aswb/src/com/google/idea/blaze/android/sdk/BlazeSdkProviderImpl.java
new file mode 100644
index 0000000..1d1a43b
--- /dev/null
+++ b/aswb/src/com/google/idea/blaze/android/sdk/BlazeSdkProviderImpl.java
@@ -0,0 +1,46 @@
+/*
+ * Copyright 2017 The Bazel Authors. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.idea.blaze.android.sdk;
+
+import com.android.tools.idea.sdk.AndroidSdks;
+import com.intellij.openapi.projectRoots.Sdk;
+import java.util.List;
+import javax.annotation.Nullable;
+import org.jetbrains.android.sdk.AndroidSdkAdditionalData;
+
+/** Indirection to Sdks for testing purposes. */
+public class BlazeSdkProviderImpl implements BlazeSdkProvider {
+  @Override
+  public List<Sdk> getAllAndroidSdks() {
+    return AndroidSdks.getInstance().getAllAndroidSdks();
+  }
+
+  @Override
+  public Sdk findSdk(String targetHash) {
+    return AndroidSdks.getInstance().findSuitableAndroidSdk(targetHash);
+  }
+
+  @Override
+  @Nullable
+  public String getSdkTargetHash(Sdk sdk) {
+    AndroidSdkAdditionalData additionalData =
+        AndroidSdks.getInstance().getAndroidSdkAdditionalData(sdk);
+    if (additionalData == null) {
+      return null;
+    }
+    return additionalData.getBuildTargetHashString();
+  }
+}
diff --git a/aswb/src/com/google/idea/blaze/android/sdk/MockBlazeSdkProvider.java b/aswb/src/com/google/idea/blaze/android/sdk/MockBlazeSdkProvider.java
new file mode 100644
index 0000000..2a97716
--- /dev/null
+++ b/aswb/src/com/google/idea/blaze/android/sdk/MockBlazeSdkProvider.java
@@ -0,0 +1,54 @@
+/*
+ * Copyright 2017 The Bazel Authors. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.idea.blaze.android.sdk;
+
+import com.google.common.collect.Maps;
+import com.intellij.openapi.projectRoots.Sdk;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.Map.Entry;
+import javax.annotation.Nullable;
+
+/** Indirection to Sdks for testing purposes. */
+public class MockBlazeSdkProvider implements BlazeSdkProvider {
+  Map<String, Sdk> sdks = Maps.newHashMap();
+
+  public void addSdk(String targetHash, Sdk sdk) {
+    sdks.put(targetHash, sdk);
+  }
+
+  @Override
+  public List<Sdk> getAllAndroidSdks() {
+    return new ArrayList<>(sdks.values());
+  }
+
+  @Override
+  public Sdk findSdk(String targetHash) {
+    return sdks.get(targetHash);
+  }
+
+  @Override
+  @Nullable
+  public String getSdkTargetHash(Sdk sdk) {
+    return sdks.entrySet()
+        .stream()
+        .filter(entry -> entry.getValue() == sdk)
+        .map(Entry::getKey)
+        .findFirst()
+        .orElse(null);
+  }
+}
diff --git a/aswb/2.3/src/com/google/idea/blaze/android/settings/BlazeAndroidUserSettingsContributor.java b/aswb/src/com/google/idea/blaze/android/settings/BlazeAndroidUserSettingsContributor.java
similarity index 100%
rename from aswb/2.3/src/com/google/idea/blaze/android/settings/BlazeAndroidUserSettingsContributor.java
rename to aswb/src/com/google/idea/blaze/android/settings/BlazeAndroidUserSettingsContributor.java
diff --git a/aswb/src/com/google/idea/blaze/android/sync/AndroidPrefetchFileSource.java b/aswb/src/com/google/idea/blaze/android/sync/AndroidPrefetchFileSource.java
index 162f693..821f098 100644
--- a/aswb/src/com/google/idea/blaze/android/sync/AndroidPrefetchFileSource.java
+++ b/aswb/src/com/google/idea/blaze/android/sync/AndroidPrefetchFileSource.java
@@ -19,6 +19,7 @@
 import com.google.idea.blaze.android.sync.model.BlazeAndroidSyncData;
 import com.google.idea.blaze.base.model.BlazeProjectData;
 import com.google.idea.blaze.base.prefetch.PrefetchFileSource;
+import com.google.idea.blaze.base.projectview.ProjectViewSet;
 import com.google.idea.blaze.base.sync.workspace.ArtifactLocationDecoder;
 import com.intellij.openapi.project.Project;
 import java.io.File;
@@ -29,7 +30,10 @@
 public class AndroidPrefetchFileSource implements PrefetchFileSource {
   @Override
   public void addFilesToPrefetch(
-      Project project, BlazeProjectData blazeProjectData, Collection<File> files) {
+      Project project,
+      ProjectViewSet projectViewSet,
+      BlazeProjectData blazeProjectData,
+      Collection<File> files) {
     BlazeAndroidSyncData syncData = blazeProjectData.syncState.get(BlazeAndroidSyncData.class);
     if (syncData == null) {
       return;
diff --git a/aswb/src/com/google/idea/blaze/android/sync/BlazeAndroidJavaSyncAugmenter.java b/aswb/src/com/google/idea/blaze/android/sync/BlazeAndroidJavaSyncAugmenter.java
index 084aec6..16afc3f 100644
--- a/aswb/src/com/google/idea/blaze/android/sync/BlazeAndroidJavaSyncAugmenter.java
+++ b/aswb/src/com/google/idea/blaze/android/sync/BlazeAndroidJavaSyncAugmenter.java
@@ -48,7 +48,7 @@
     }
     LibraryArtifact idlJar = androidIdeInfo.idlJar;
     if (idlJar != null) {
-      genJars.add(new BlazeJarLibrary(idlJar, target.key));
+      genJars.add(new BlazeJarLibrary(idlJar));
     }
     Set<String> whitelistedGenResourcePaths =
         projectViewSet
@@ -66,7 +66,7 @@
       if (!discardResourceJar) {
         LibraryArtifact resourceJar = androidIdeInfo.resourceJar;
         if (resourceJar != null) {
-          jars.add(new BlazeJarLibrary(resourceJar, target.key));
+          jars.add(new BlazeJarLibrary(resourceJar));
         }
       }
     }
diff --git a/aswb/src/com/google/idea/blaze/android/sync/BlazeAndroidLibrarySource.java b/aswb/src/com/google/idea/blaze/android/sync/BlazeAndroidLibrarySource.java
index bfff837..73bb993 100644
--- a/aswb/src/com/google/idea/blaze/android/sync/BlazeAndroidLibrarySource.java
+++ b/aswb/src/com/google/idea/blaze/android/sync/BlazeAndroidLibrarySource.java
@@ -19,13 +19,8 @@
 import com.google.idea.blaze.android.sync.model.BlazeAndroidSyncData;
 import com.google.idea.blaze.base.model.BlazeLibrary;
 import com.google.idea.blaze.base.model.BlazeProjectData;
-import com.google.idea.blaze.base.projectview.section.Glob;
-import com.google.idea.blaze.base.projectview.section.Glob.GlobSet;
 import com.google.idea.blaze.base.sync.libraries.LibrarySource;
-import com.google.idea.blaze.java.sync.LibraryGlobFilter;
 import java.util.Collection;
-import java.util.function.Predicate;
-import javax.annotation.Nullable;
 
 class BlazeAndroidLibrarySource extends LibrarySource.Adapter {
   private final BlazeProjectData blazeProjectData;
@@ -46,12 +41,4 @@
     }
     return libraries.build();
   }
-
-  @Nullable
-  @Override
-  public Predicate<BlazeLibrary> getLibraryFilter() {
-    // This is supplied via the SDK
-    GlobSet globSet = new GlobSet(ImmutableList.of(new Glob("*/android_blaze.jar")));
-    return new LibraryGlobFilter(globSet);
-  }
 }
diff --git a/aswb/src/com/google/idea/blaze/android/sync/BlazeAndroidSyncPlugin.java b/aswb/src/com/google/idea/blaze/android/sync/BlazeAndroidSyncPlugin.java
index bee4d9a..f21d144 100644
--- a/aswb/src/com/google/idea/blaze/android/sync/BlazeAndroidSyncPlugin.java
+++ b/aswb/src/com/google/idea/blaze/android/sync/BlazeAndroidSyncPlugin.java
@@ -15,20 +15,21 @@
  */
 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.AndroidMinSdkSection;
 import com.google.idea.blaze.android.projectview.AndroidSdkPlatformSection;
 import com.google.idea.blaze.android.projectview.GeneratedAndroidResourcesSection;
+import com.google.idea.blaze.android.sdk.BlazeSdkProvider;
 import com.google.idea.blaze.android.sync.importer.BlazeAndroidWorkspaceImporter;
 import com.google.idea.blaze.android.sync.model.AndroidSdkPlatform;
 import com.google.idea.blaze.android.sync.model.BlazeAndroidImportResult;
 import com.google.idea.blaze.android.sync.model.BlazeAndroidSyncData;
 import com.google.idea.blaze.android.sync.projectstructure.BlazeAndroidProjectStructureSyncer;
 import com.google.idea.blaze.android.sync.sdk.AndroidSdkFromProjectView;
+import com.google.idea.blaze.base.command.info.BlazeInfo;
 import com.google.idea.blaze.base.ideinfo.TargetMap;
 import com.google.idea.blaze.base.model.BlazeProjectData;
 import com.google.idea.blaze.base.model.BlazeVersionData;
@@ -49,7 +50,6 @@
 import com.google.idea.blaze.base.sync.libraries.LibrarySource;
 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 com.google.idea.blaze.java.projectview.JavaLanguageLevelSection;
@@ -111,13 +111,17 @@
 
   @Override
   public void installSdks(BlazeContext context) {
-    File path = IdeSdks.getAndroidSdkPath();
+    if (ApplicationManager.getApplication().isUnitTestMode()) {
+      return;
+    }
+
+    File path = IdeSdks.getInstance().getAndroidSdkPath();
     if (path != null) {
       context.output(new StatusOutput("Installing SDK platforms..."));
       ApplicationManager.getApplication()
           .invokeAndWait(
               () -> {
-                IdeSdks.createAndroidSdkPerAndroidTarget(path);
+                IdeSdks.getInstance().createAndroidSdkPerAndroidTarget(path);
               },
               ModalityState.defaultModalityState());
     }
@@ -130,7 +134,7 @@
       WorkspaceRoot workspaceRoot,
       ProjectViewSet projectViewSet,
       WorkspaceLanguageSettings workspaceLanguageSettings,
-      BlazeRoots blazeRoots,
+      BlazeInfo blazeInfo,
       @Nullable WorkingSet workingSet,
       WorkspacePathResolver workspacePathResolver,
       ArtifactLocationDecoder artifactLocationDecoder,
@@ -176,7 +180,7 @@
     if (androidSdkPlatform == null) {
       return;
     }
-    Sdk sdk = AndroidSdkUtils.findSuitableAndroidSdk(androidSdkPlatform.androidSdk);
+    Sdk sdk = BlazeSdkProvider.getInstance().findSdk(androidSdkPlatform.androidSdk);
     if (sdk == null) {
       IssueOutput.error(
               String.format("Android platform '%s' not found.", androidSdkPlatform.androidSdk))
@@ -260,6 +264,7 @@
 
   @Override
   public boolean validateProjectView(
+      @Nullable Project project,
       BlazeContext context,
       ProjectViewSet projectViewSet,
       WorkspaceLanguageSettings workspaceLanguageSettings) {
@@ -306,7 +311,8 @@
 
   @Nullable
   @Override
-  public LibrarySource getLibrarySource(BlazeProjectData blazeProjectData) {
+  public LibrarySource getLibrarySource(
+      ProjectViewSet projectViewSet, BlazeProjectData blazeProjectData) {
     if (!isAndroidWorkspace(blazeProjectData.workspaceLanguageSettings)) {
       return null;
     }
diff --git a/aswb/src/com/google/idea/blaze/android/sync/model/AndroidResourceModule.java b/aswb/src/com/google/idea/blaze/android/sync/model/AndroidResourceModule.java
index 73d76f5..8a2cca5 100644
--- a/aswb/src/com/google/idea/blaze/android/sync/model/AndroidResourceModule.java
+++ b/aswb/src/com/google/idea/blaze/android/sync/model/AndroidResourceModule.java
@@ -145,7 +145,7 @@
     }
 
     public Builder addTransitiveResourceDependency(String dependency) {
-      return addTransitiveResourceDependency(new Label(dependency));
+      return addTransitiveResourceDependency(Label.create(dependency));
     }
 
     @NotNull
diff --git a/aswb/2.3/src/com/google/idea/blaze/android/sync/model/idea/BlazeClassJarProvider.java b/aswb/src/com/google/idea/blaze/android/sync/model/idea/BlazeClassJarProvider.java
similarity index 100%
rename from aswb/2.3/src/com/google/idea/blaze/android/sync/model/idea/BlazeClassJarProvider.java
rename to aswb/src/com/google/idea/blaze/android/sync/model/idea/BlazeClassJarProvider.java
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 541be52..7f04fe2 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,7 +16,6 @@
 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;
@@ -56,7 +55,7 @@
   private static void configureFacet(AndroidFacet facet) {
     JpsAndroidModuleProperties facetState = facet.getProperties();
     facetState.ALLOW_USER_CONFIGURATION = false;
-    Compatibility.setFacetStateIsLibraryProject(facetState);
+    facetState.PROJECT_TYPE = AndroidProject.PROJECT_TYPE_LIBRARY;
     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 66b020f..29a20a6 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
@@ -49,12 +49,13 @@
 import com.google.idea.blaze.base.sync.BlazeSyncPlugin;
 import com.google.idea.blaze.base.sync.data.BlazeProjectDataManager;
 import com.google.idea.blaze.base.sync.projectstructure.ModuleEditorProvider;
+import com.google.idea.blaze.base.sync.projectstructure.ModuleFinder;
 import com.google.idea.blaze.base.sync.workspace.ArtifactLocationDecoder;
 import com.intellij.execution.RunManager;
 import com.intellij.execution.configurations.RunConfiguration;
 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.StdModuleTypes;
 import com.intellij.openapi.project.Project;
 import com.intellij.openapi.roots.ModifiableRootModel;
@@ -69,6 +70,8 @@
 /** Updates the IDE's project structure. */
 public class BlazeAndroidProjectStructureSyncer {
 
+  private static final Logger logger = Logger.getInstance(BlazeAndroidProjectStructureSyncer.class);
+
   public static void updateProjectStructure(
       Project project,
       BlazeContext context,
@@ -214,7 +217,7 @@
   public static Module ensureRunConfigurationModule(Project project, Label label) {
     TargetKey targetKey = TargetKey.forPlainTarget(label);
     String moduleName = moduleNameForAndroidModule(targetKey);
-    Module module = ModuleManager.getInstance(project).findModuleByName(moduleName);
+    Module module = ModuleFinder.getInstance(project).findModuleByName(moduleName);
     if (module != null) {
       return module;
     }
@@ -308,12 +311,16 @@
         project, workspaceRoot, workspaceModule, androidSdkPlatform);
 
     ArtifactLocationDecoder artifactLocationDecoder = blazeProjectData.artifactLocationDecoder;
-    ModuleManager moduleManager = ModuleManager.getInstance(project);
+    ModuleFinder moduleFinder = ModuleFinder.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);
+      Module module = moduleFinder.findModuleByName(moduleName);
+      if (module == null) {
+        logger.warn("No module found for resource target: " + target.key);
+        continue;
+      }
       registry.put(module, androidResourceModule);
 
       AndroidIdeInfo androidIdeInfo = target.androidIdeInfo;
@@ -345,7 +352,11 @@
             project, projectViewSet, blazeProjectData, androidResourceModules);
     for (TargetIdeInfo target : runConfigurationTargets) {
       String moduleName = moduleNameForAndroidModule(target.key);
-      Module module = moduleManager.findModuleByName(moduleName);
+      Module module = moduleFinder.findModuleByName(moduleName);
+      if (module == null) {
+        logger.warn("No module found for run configuration target: " + target.key);
+        continue;
+      }
       AndroidIdeInfo androidIdeInfo = target.androidIdeInfo;
       assert androidIdeInfo != null;
       updateModuleFacetInMemoryState(
diff --git a/aswb/src/com/google/idea/blaze/android/sync/sdk/AndroidSdkFromProjectView.java b/aswb/src/com/google/idea/blaze/android/sync/sdk/AndroidSdkFromProjectView.java
index b47e238..c4eb537 100644
--- a/aswb/src/com/google/idea/blaze/android/sync/sdk/AndroidSdkFromProjectView.java
+++ b/aswb/src/com/google/idea/blaze/android/sync/sdk/AndroidSdkFromProjectView.java
@@ -18,9 +18,9 @@
 import com.google.common.base.Joiner;
 import com.google.common.collect.Lists;
 import com.google.common.collect.Sets;
-import com.google.idea.blaze.android.compatibility.Compatibility.AndroidSdkUtils;
 import com.google.idea.blaze.android.projectview.AndroidMinSdkSection;
 import com.google.idea.blaze.android.projectview.AndroidSdkPlatformSection;
+import com.google.idea.blaze.android.sdk.BlazeSdkProvider;
 import com.google.idea.blaze.android.sync.model.AndroidSdkPlatform;
 import com.google.idea.blaze.base.projectview.ProjectViewSet;
 import com.google.idea.blaze.base.projectview.ProjectViewSet.ProjectViewFile;
@@ -40,7 +40,7 @@
   @Nullable
   public static AndroidSdkPlatform getAndroidSdkPlatform(
       BlazeContext context, ProjectViewSet projectViewSet) {
-    Collection<Sdk> sdks = AndroidSdkUtils.getAllAndroidSdks();
+    List<Sdk> sdks = BlazeSdkProvider.getInstance().getAllAndroidSdks();
     if (sdks.isEmpty()) {
       IssueOutput.error("No Android SDK configured. Please use the SDK manager to configure.")
           .navigatable(
@@ -81,7 +81,7 @@
       return null;
     }
 
-    Sdk sdk = AndroidSdkUtils.findSuitableAndroidSdk(androidSdk);
+    Sdk sdk = BlazeSdkProvider.getInstance().findSdk(androidSdk);
     if (sdk == null) {
       ProjectViewFile projectViewFile = projectViewSet.getTopLevelProjectViewFile();
       IssueOutput.error(
@@ -104,19 +104,10 @@
     return new AndroidSdkPlatform(androidSdk, androidMinSdk);
   }
 
-  @Nullable
-  public static String getSdkTargetHash(Sdk sdk) {
-    AndroidSdkAdditionalData additionalData = AndroidSdkUtils.getAndroidSdkAdditionalData(sdk);
-    if (additionalData == null) {
-      return null;
-    }
-    return additionalData.getBuildTargetHashString();
-  }
-
   public static List<String> getAvailableSdkTargetHashes(Collection<Sdk> sdks) {
     Set<String> names = Sets.newHashSet();
     for (Sdk sdk : sdks) {
-      String targetHash = getSdkTargetHash(sdk);
+      String targetHash = BlazeSdkProvider.getInstance().getSdkTargetHash(sdk);
       if (targetHash != null) {
         names.add(targetHash);
       }
diff --git a/aswb/src/com/google/idea/blaze/android/sync/sdk/SdkUtil.java b/aswb/src/com/google/idea/blaze/android/sync/sdk/SdkUtil.java
index fcbe1f9..885b931 100644
--- a/aswb/src/com/google/idea/blaze/android/sync/sdk/SdkUtil.java
+++ b/aswb/src/com/google/idea/blaze/android/sync/sdk/SdkUtil.java
@@ -16,7 +16,7 @@
 package com.google.idea.blaze.android.sync.sdk;
 
 import com.android.tools.idea.updater.configure.SdkUpdaterConfigurableProvider;
-import com.google.idea.blaze.android.compatibility.Compatibility.AndroidSdkUtils;
+import com.google.idea.blaze.android.sdk.BlazeSdkProvider;
 import com.google.idea.blaze.android.sync.model.AndroidSdkPlatform;
 import com.google.idea.blaze.android.sync.model.BlazeAndroidSyncData;
 import com.google.idea.blaze.base.model.BlazeProjectData;
@@ -49,7 +49,7 @@
     if (androidSdkPlatform == null) {
       return null;
     }
-    Sdk sdk = AndroidSdkUtils.findSuitableAndroidSdk(androidSdkPlatform.androidSdk);
+    Sdk sdk = BlazeSdkProvider.getInstance().findSdk(androidSdkPlatform.androidSdk);
     if (sdk == null) {
       return null;
     }
diff --git a/aswb/tests/integrationtests/com/google/idea/blaze/android/run/BlazeAndroidRunConfigurationCommonStateMigrationTest.java b/aswb/tests/integrationtests/com/google/idea/blaze/android/run/BlazeAndroidRunConfigurationCommonStateMigrationTest.java
deleted file mode 100644
index 7eca0c6..0000000
--- a/aswb/tests/integrationtests/com/google/idea/blaze/android/run/BlazeAndroidRunConfigurationCommonStateMigrationTest.java
+++ /dev/null
@@ -1,131 +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;
-
-import static com.google.common.truth.Truth.assertThat;
-
-import com.google.idea.blaze.android.BlazeAndroidIntegrationTestCase;
-import java.io.StringReader;
-import java.util.List;
-import org.jdom.Element;
-import org.jdom.input.SAXBuilder;
-import org.jdom.output.Format;
-import org.jdom.output.XMLOutputter;
-import org.junit.Before;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.junit.runners.JUnit4;
-
-/**
- * Tests for migrating the storage of deploy target state and debugger state in {@link
- * BlazeAndroidRunConfigurationCommonState} TODO Introduced in 1.12, remove in 1.14 when the
- * migration code in BlazeAndroidRunConfigurationCommonState is removed.
- */
-@RunWith(JUnit4.class)
-public class BlazeAndroidRunConfigurationCommonStateMigrationTest
-    extends BlazeAndroidIntegrationTestCase {
-
-  private static final String DEPLOY_TARGET_STATES_RAW_XML =
-      "<android-deploy-target-states>"
-          + "  <option name=\"USE_LAST_SELECTED_DEVICE\" value=\"true\" />"
-          + "  <option name=\"PREFERRED_AVD\" value=\"some avd\" />"
-          + "</android-deploy-target-states>";
-  private static final String DEBUGGER_STATE_AUTO_RAW_XML =
-      "<Auto>"
-          + "  <option name=\"USE_JAVA_AWARE_DEBUGGER\" value=\"true\" />"
-          + "  <option name=\"WORKING_DIR\" value=\"/some/directory\" />"
-          + "  <option name=\"TARGET_LOGGING_CHANNELS\" value=\"some channels\" />"
-          + "</Auto>";
-  private static final String DEBUGGER_STATE_NATIVE_RAW_XML =
-      "<Native>"
-          + "  <option name=\"USE_JAVA_AWARE_DEBUGGER\" value=\"false\" />"
-          + "  <option name=\"WORKING_DIR\" value=\"\" />"
-          + "  <option name=\"TARGET_LOGGING_CHANNELS\""
-          + "          value=\"lldb process:gdb-remote packets\" />"
-          + "</Native>";
-  private static final String DEBUGGER_STATE_JAVA_RAW_XML = "<Java />";
-  private static final String DEBUGGER_STATE_HYBRID_RAW_XML =
-      "<Hybrid>"
-          + "  <option name=\"USE_JAVA_AWARE_DEBUGGER\" value=\"true\" />"
-          + "  <option name=\"WORKING_DIR\" value=\"\" />"
-          + "  <option name=\"TARGET_LOGGING_CHANNELS\""
-          + "          value=\"lldb process:gdb-remote packets\" />"
-          + "</Hybrid>";
-  private static final String DEBUGGER_STATE_BLAZE_AUTO_RAW_XML =
-      "<BlazeAuto>"
-          + "  <option name=\"USE_JAVA_AWARE_DEBUGGER\" value=\"false\" />"
-          + "  <option name=\"WORKING_DIR\" value=\"/some/other/directory\" />"
-          + "  <option name=\"TARGET_LOGGING_CHANNELS\" value=\"some other channels\" />"
-          + "</BlazeAuto>";
-
-  private BlazeAndroidRunConfigurationCommonState state;
-  private SAXBuilder saxBuilder;
-  private XMLOutputter xmlOutputter;
-
-  @Before
-  public final void doSetup() throws Exception {
-    state = new BlazeAndroidRunConfigurationCommonState(buildSystem().getName(), false);
-    saxBuilder = new SAXBuilder();
-    xmlOutputter = new XMLOutputter(Format.getCompactFormat());
-  }
-
-  private String formatRawXml(String rawXml) throws Exception {
-    Element element =
-        saxBuilder.build(new StringReader("<?xml version=\"1.0\"?>" + rawXml)).getRootElement();
-    return xmlOutputter.outputString(element);
-  }
-
-  @Test
-  public void readAndWriteShouldRemoveExtraElements() 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
-            + "  <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);
-    List<Element> flagElements = migratedElement.getChildren("blaze-user-flag");
-    assertThat(flagElements).hasSize(2);
-
-    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);
-  }
-}
diff --git a/aswb/tests/integrationtests/com/google/idea/blaze/android/run/BlazeAndroidRunConfigurationCommonStateTest.java b/aswb/tests/integrationtests/com/google/idea/blaze/android/run/BlazeAndroidRunConfigurationCommonStateTest.java
index 1964580..0d9cba6 100644
--- a/aswb/tests/integrationtests/com/google/idea/blaze/android/run/BlazeAndroidRunConfigurationCommonStateTest.java
+++ b/aswb/tests/integrationtests/com/google/idea/blaze/android/run/BlazeAndroidRunConfigurationCommonStateTest.java
@@ -19,8 +19,9 @@
 import static com.google.common.truth.Truth.assertThat;
 
 import com.google.common.collect.ImmutableList;
-import com.google.idea.blaze.android.BlazeAndroidIntegrationTestCase;
+import com.google.idea.blaze.android.AndroidIntegrationTestSetupRule;
 import com.google.idea.blaze.android.cppapi.NdkSupport;
+import com.google.idea.blaze.base.BlazeIntegrationTestCase;
 import com.google.idea.common.experiments.ExperimentService;
 import com.google.idea.common.experiments.MockExperimentService;
 import com.intellij.openapi.util.InvalidDataException;
@@ -29,14 +30,18 @@
 import org.jdom.output.Format;
 import org.jdom.output.XMLOutputter;
 import org.junit.Before;
+import org.junit.Rule;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.junit.runners.JUnit4;
 
 /** Tests for {@link BlazeAndroidRunConfigurationCommonState}. */
 @RunWith(JUnit4.class)
-public class BlazeAndroidRunConfigurationCommonStateTest extends BlazeAndroidIntegrationTestCase {
+public class BlazeAndroidRunConfigurationCommonStateTest extends BlazeIntegrationTestCase {
 
+  @Rule
+  public final AndroidIntegrationTestSetupRule androidSetupRule =
+      new AndroidIntegrationTestSetupRule();
   private BlazeAndroidRunConfigurationCommonState state;
 
   @Before
@@ -52,7 +57,7 @@
 
   @Test
   public void readAndWriteShouldMatch() throws InvalidDataException, WriteExternalException {
-    state.setUserFlags(ImmutableList.of("--flag1", "--flag2"));
+    state.getBlazeFlagsState().setRawFlags(ImmutableList.of("--flag1", "--flag2"));
     state.setNativeDebuggingEnabled(true);
 
     Element element = new Element("test");
@@ -61,7 +66,9 @@
         new BlazeAndroidRunConfigurationCommonState(buildSystem().getName(), false);
     readState.readExternal(element);
 
-    assertThat(readState.getUserFlags()).containsExactly("--flag1", "--flag2").inOrder();
+    assertThat(readState.getBlazeFlagsState().getRawFlags())
+        .containsExactly("--flag1", "--flag2")
+        .inOrder();
     assertThat(readState.isNativeDebuggingEnabled()).isTrue();
   }
 
@@ -73,13 +80,16 @@
         new BlazeAndroidRunConfigurationCommonState(buildSystem().getName(), false);
     readState.readExternal(element);
 
-    assertThat(readState.getUserFlags()).isEqualTo(state.getUserFlags());
+    assertThat(readState.getBlazeFlagsState().getRawFlags())
+        .isEqualTo(state.getBlazeFlagsState().getRawFlags());
     assertThat(readState.isNativeDebuggingEnabled()).isEqualTo(state.isNativeDebuggingEnabled());
   }
 
   @Test
   public void readShouldOmitEmptyFlags() throws InvalidDataException, WriteExternalException {
-    state.setUserFlags(ImmutableList.of("hi ", "", "I'm", " ", "\t", "Josh\r\n", "\n"));
+    state
+        .getBlazeFlagsState()
+        .setRawFlags(ImmutableList.of("hi ", "", "I'm", " ", "\t", "Josh\r\n", "\n"));
 
     Element element = new Element("test");
     state.writeExternal(element);
@@ -87,14 +97,16 @@
         new BlazeAndroidRunConfigurationCommonState(buildSystem().getName(), false);
     readState.readExternal(element);
 
-    assertThat(readState.getUserFlags()).containsExactly("hi", "I'm", "Josh").inOrder();
+    assertThat(readState.getBlazeFlagsState().getRawFlags())
+        .containsExactly("hi", "I'm", "Josh")
+        .inOrder();
   }
 
   @Test
   public void repeatedWriteShouldNotChangeElement() throws WriteExternalException {
     final XMLOutputter xmlOutputter = new XMLOutputter(Format.getCompactFormat());
 
-    state.setUserFlags(ImmutableList.of("--flag1", "--flag2"));
+    state.getBlazeFlagsState().setRawFlags(ImmutableList.of("--flag1", "--flag2"));
     state.setNativeDebuggingEnabled(true);
 
     Element firstWrite = new Element("test");
diff --git a/aswb/tests/integrationtests/com/google/idea/blaze/android/run/binary/BlazeAndroidBinaryRunConfigurationStateTest.java b/aswb/tests/integrationtests/com/google/idea/blaze/android/run/binary/BlazeAndroidBinaryRunConfigurationStateTest.java
index 05de9a5..e72c5de 100644
--- a/aswb/tests/integrationtests/com/google/idea/blaze/android/run/binary/BlazeAndroidBinaryRunConfigurationStateTest.java
+++ b/aswb/tests/integrationtests/com/google/idea/blaze/android/run/binary/BlazeAndroidBinaryRunConfigurationStateTest.java
@@ -19,9 +19,10 @@
 import static com.google.common.truth.Truth.assertThat;
 
 import com.google.common.collect.ImmutableList;
-import com.google.idea.blaze.android.BlazeAndroidIntegrationTestCase;
+import com.google.idea.blaze.android.AndroidIntegrationTestSetupRule;
 import com.google.idea.blaze.android.cppapi.NdkSupport;
 import com.google.idea.blaze.android.run.BlazeAndroidRunConfigurationCommonState;
+import com.google.idea.blaze.base.BlazeIntegrationTestCase;
 import com.google.idea.blaze.base.run.state.RunConfigurationStateEditor;
 import com.google.idea.common.experiments.ExperimentService;
 import com.google.idea.common.experiments.MockExperimentService;
@@ -32,14 +33,18 @@
 import org.jdom.output.Format;
 import org.jdom.output.XMLOutputter;
 import org.junit.Before;
+import org.junit.Rule;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.junit.runners.JUnit4;
 
 /** Tests for {@link BlazeAndroidBinaryRunConfigurationState}. */
 @RunWith(JUnit4.class)
-public class BlazeAndroidBinaryRunConfigurationStateTest extends BlazeAndroidIntegrationTestCase {
+public class BlazeAndroidBinaryRunConfigurationStateTest extends BlazeIntegrationTestCase {
 
+  @Rule
+  public final AndroidIntegrationTestSetupRule androidSetupRule =
+      new AndroidIntegrationTestSetupRule();
   private BlazeAndroidBinaryRunConfigurationState state;
 
   @Before
@@ -56,7 +61,7 @@
   @Test
   public void readAndWriteShouldMatch() throws InvalidDataException, WriteExternalException {
     BlazeAndroidRunConfigurationCommonState commonState = state.getCommonState();
-    commonState.setUserFlags(ImmutableList.of("--flag1", "--flag2"));
+    commonState.getBlazeFlagsState().setRawFlags(ImmutableList.of("--flag1", "--flag2"));
     commonState.setNativeDebuggingEnabled(true);
 
     state.setActivityClass("com.example.TestActivity");
@@ -74,7 +79,9 @@
     readState.readExternal(element);
 
     BlazeAndroidRunConfigurationCommonState readCommonState = readState.getCommonState();
-    assertThat(readCommonState.getUserFlags()).containsExactly("--flag1", "--flag2").inOrder();
+    assertThat(readCommonState.getBlazeFlagsState().getRawFlags())
+        .containsExactly("--flag1", "--flag2")
+        .inOrder();
     assertThat(readCommonState.isNativeDebuggingEnabled()).isTrue();
 
     assertThat(readState.getActivityClass()).isEqualTo("com.example.TestActivity");
@@ -97,7 +104,8 @@
 
     BlazeAndroidRunConfigurationCommonState commonState = state.getCommonState();
     BlazeAndroidRunConfigurationCommonState readCommonState = readState.getCommonState();
-    assertThat(readCommonState.getUserFlags()).isEqualTo(commonState.getUserFlags());
+    assertThat(readCommonState.getBlazeFlagsState().getRawFlags())
+        .isEqualTo(commonState.getBlazeFlagsState().getRawFlags());
     assertThat(readCommonState.isNativeDebuggingEnabled())
         .isEqualTo(commonState.isNativeDebuggingEnabled());
 
@@ -115,7 +123,7 @@
     final XMLOutputter xmlOutputter = new XMLOutputter(Format.getCompactFormat());
 
     BlazeAndroidRunConfigurationCommonState commonState = state.getCommonState();
-    commonState.setUserFlags(ImmutableList.of("--flag1", "--flag2"));
+    commonState.getBlazeFlagsState().setRawFlags(ImmutableList.of("--flag1", "--flag2"));
     commonState.setNativeDebuggingEnabled(true);
 
     state.setActivityClass("com.example.TestActivity");
@@ -140,7 +148,7 @@
     RunConfigurationStateEditor editor = state.getEditor(getProject());
 
     BlazeAndroidRunConfigurationCommonState commonState = state.getCommonState();
-    commonState.setUserFlags(ImmutableList.of("--flag1", "--flag2"));
+    commonState.getBlazeFlagsState().setRawFlags(ImmutableList.of("--flag1", "--flag2"));
     commonState.setNativeDebuggingEnabled(true);
 
     state.setActivityClass("com.example.TestActivity");
@@ -158,7 +166,8 @@
     editor.applyEditorTo(readState);
 
     BlazeAndroidRunConfigurationCommonState readCommonState = readState.getCommonState();
-    assertThat(readCommonState.getUserFlags()).isEqualTo(commonState.getUserFlags());
+    assertThat(readCommonState.getBlazeFlagsState().getRawFlags())
+        .isEqualTo(commonState.getBlazeFlagsState().getRawFlags());
     assertThat(readCommonState.isNativeDebuggingEnabled())
         .isEqualTo(commonState.isNativeDebuggingEnabled());
 
@@ -183,7 +192,8 @@
 
     BlazeAndroidRunConfigurationCommonState commonState = state.getCommonState();
     BlazeAndroidRunConfigurationCommonState readCommonState = readState.getCommonState();
-    assertThat(readCommonState.getUserFlags()).isEqualTo(commonState.getUserFlags());
+    assertThat(readCommonState.getBlazeFlagsState().getRawFlags())
+        .isEqualTo(commonState.getBlazeFlagsState().getRawFlags());
     assertThat(readCommonState.isNativeDebuggingEnabled())
         .isEqualTo(commonState.isNativeDebuggingEnabled());
 
diff --git a/aswb/tests/integrationtests/com/google/idea/blaze/android/run/test/BlazeAndroidTestRunConfigurationStateTest.java b/aswb/tests/integrationtests/com/google/idea/blaze/android/run/test/BlazeAndroidTestRunConfigurationStateTest.java
index f04a14e..4944264 100644
--- a/aswb/tests/integrationtests/com/google/idea/blaze/android/run/test/BlazeAndroidTestRunConfigurationStateTest.java
+++ b/aswb/tests/integrationtests/com/google/idea/blaze/android/run/test/BlazeAndroidTestRunConfigurationStateTest.java
@@ -18,9 +18,10 @@
 import static com.google.common.truth.Truth.assertThat;
 
 import com.google.common.collect.ImmutableList;
-import com.google.idea.blaze.android.BlazeAndroidIntegrationTestCase;
+import com.google.idea.blaze.android.AndroidIntegrationTestSetupRule;
 import com.google.idea.blaze.android.cppapi.NdkSupport;
 import com.google.idea.blaze.android.run.BlazeAndroidRunConfigurationCommonState;
+import com.google.idea.blaze.base.BlazeIntegrationTestCase;
 import com.google.idea.blaze.base.run.state.RunConfigurationStateEditor;
 import com.google.idea.common.experiments.ExperimentService;
 import com.google.idea.common.experiments.MockExperimentService;
@@ -31,14 +32,18 @@
 import org.jdom.output.Format;
 import org.jdom.output.XMLOutputter;
 import org.junit.Before;
+import org.junit.Rule;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.junit.runners.JUnit4;
 
 /** Tests for {@link BlazeAndroidTestRunConfigurationState}. */
 @RunWith(JUnit4.class)
-public class BlazeAndroidTestRunConfigurationStateTest extends BlazeAndroidIntegrationTestCase {
+public class BlazeAndroidTestRunConfigurationStateTest extends BlazeIntegrationTestCase {
 
+  @Rule
+  public final AndroidIntegrationTestSetupRule androidSetupRule =
+      new AndroidIntegrationTestSetupRule();
   private BlazeAndroidTestRunConfigurationState state;
 
   @Before
@@ -55,7 +60,7 @@
   @Test
   public void readAndWriteShouldMatch() throws InvalidDataException, WriteExternalException {
     BlazeAndroidRunConfigurationCommonState commonState = state.getCommonState();
-    commonState.setUserFlags(ImmutableList.of("--flag1", "--flag2"));
+    commonState.getBlazeFlagsState().setRawFlags(ImmutableList.of("--flag1", "--flag2"));
     commonState.setNativeDebuggingEnabled(true);
 
     state.setTestingType(BlazeAndroidTestRunConfigurationState.TEST_METHOD);
@@ -73,7 +78,9 @@
     readState.readExternal(element);
 
     BlazeAndroidRunConfigurationCommonState readCommonState = readState.getCommonState();
-    assertThat(readCommonState.getUserFlags()).containsExactly("--flag1", "--flag2").inOrder();
+    assertThat(readCommonState.getBlazeFlagsState().getRawFlags())
+        .containsExactly("--flag1", "--flag2")
+        .inOrder();
     assertThat(readCommonState.isNativeDebuggingEnabled()).isTrue();
 
     assertThat(readState.getTestingType())
@@ -96,7 +103,8 @@
 
     BlazeAndroidRunConfigurationCommonState commonState = state.getCommonState();
     BlazeAndroidRunConfigurationCommonState readCommonState = readState.getCommonState();
-    assertThat(readCommonState.getUserFlags()).isEqualTo(commonState.getUserFlags());
+    assertThat(readCommonState.getBlazeFlagsState().getRawFlags())
+        .isEqualTo(commonState.getBlazeFlagsState().getRawFlags());
     assertThat(readCommonState.isNativeDebuggingEnabled())
         .isEqualTo(commonState.isNativeDebuggingEnabled());
 
@@ -115,7 +123,7 @@
     final XMLOutputter xmlOutputter = new XMLOutputter(Format.getCompactFormat());
 
     BlazeAndroidRunConfigurationCommonState commonState = state.getCommonState();
-    commonState.setUserFlags(ImmutableList.of("--flag1", "--flag2"));
+    commonState.getBlazeFlagsState().setRawFlags(ImmutableList.of("--flag1", "--flag2"));
     commonState.setNativeDebuggingEnabled(true);
 
     state.setTestingType(BlazeAndroidTestRunConfigurationState.TEST_METHOD);
@@ -140,7 +148,7 @@
     RunConfigurationStateEditor editor = state.getEditor(getProject());
 
     BlazeAndroidRunConfigurationCommonState commonState = state.getCommonState();
-    commonState.setUserFlags(ImmutableList.of("--flag1", "--flag2"));
+    commonState.getBlazeFlagsState().setRawFlags(ImmutableList.of("--flag1", "--flag2"));
     commonState.setNativeDebuggingEnabled(true);
 
     state.setTestingType(BlazeAndroidTestRunConfigurationState.TEST_METHOD);
@@ -158,7 +166,8 @@
     editor.applyEditorTo(readState);
 
     BlazeAndroidRunConfigurationCommonState readCommonState = readState.getCommonState();
-    assertThat(readCommonState.getUserFlags()).isEqualTo(commonState.getUserFlags());
+    assertThat(readCommonState.getBlazeFlagsState().getRawFlags())
+        .isEqualTo(commonState.getBlazeFlagsState().getRawFlags());
     assertThat(readCommonState.isNativeDebuggingEnabled())
         .isEqualTo(commonState.isNativeDebuggingEnabled());
 
@@ -184,7 +193,8 @@
 
     BlazeAndroidRunConfigurationCommonState commonState = state.getCommonState();
     BlazeAndroidRunConfigurationCommonState readCommonState = readState.getCommonState();
-    assertThat(readCommonState.getUserFlags()).isEqualTo(commonState.getUserFlags());
+    assertThat(readCommonState.getBlazeFlagsState().getRawFlags())
+        .isEqualTo(commonState.getBlazeFlagsState().getRawFlags());
     assertThat(readCommonState.isNativeDebuggingEnabled())
         .isEqualTo(commonState.isNativeDebuggingEnabled());
 
diff --git a/aswb/tests/integrationtests/com/google/idea/blaze/android/run/test/smrunner/BlazeAndroidTestEventsHandlerTest.java b/aswb/tests/integrationtests/com/google/idea/blaze/android/run/test/smrunner/BlazeAndroidTestEventsHandlerTest.java
index 474e1b4..e888374 100644
--- a/aswb/tests/integrationtests/com/google/idea/blaze/android/run/test/smrunner/BlazeAndroidTestEventsHandlerTest.java
+++ b/aswb/tests/integrationtests/com/google/idea/blaze/android/run/test/smrunner/BlazeAndroidTestEventsHandlerTest.java
@@ -18,7 +18,8 @@
 import static com.google.common.truth.Truth.assertThat;
 
 import com.google.common.collect.Iterables;
-import com.google.idea.blaze.android.BlazeAndroidIntegrationTestCase;
+import com.google.idea.blaze.android.AndroidIntegrationTestSetupRule;
+import com.google.idea.blaze.base.BlazeIntegrationTestCase;
 import com.google.idea.blaze.base.model.primitives.WorkspacePath;
 import com.intellij.execution.Location;
 import com.intellij.openapi.vfs.VirtualFileManager;
@@ -29,14 +30,18 @@
 import com.intellij.psi.search.GlobalSearchScope;
 import javax.annotation.Nullable;
 import org.junit.Before;
+import org.junit.Rule;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.junit.runners.JUnit4;
 
 /** Integration tests for {@link BlazeAndroidTestEventsHandler}. */
 @RunWith(JUnit4.class)
-public class BlazeAndroidTestEventsHandlerTest extends BlazeAndroidIntegrationTestCase {
+public class BlazeAndroidTestEventsHandlerTest extends BlazeIntegrationTestCase {
 
+  @Rule
+  public final AndroidIntegrationTestSetupRule androidSetupRule =
+      new AndroidIntegrationTestSetupRule();
   private final BlazeAndroidTestEventsHandler handler = new BlazeAndroidTestEventsHandler();
 
   @Before
diff --git a/aswb/tests/integrationtests/com/google/idea/blaze/android/sync/AndroidSyncTest.java b/aswb/tests/integrationtests/com/google/idea/blaze/android/sync/AndroidSyncTest.java
new file mode 100644
index 0000000..a712648
--- /dev/null
+++ b/aswb/tests/integrationtests/com/google/idea/blaze/android/sync/AndroidSyncTest.java
@@ -0,0 +1,398 @@
+/*
+ * Copyright 2017 The Bazel Authors. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.idea.blaze.android.sync;
+
+import static com.google.common.truth.Truth.assertThat;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+import com.google.common.collect.ImmutableList;
+import com.google.idea.blaze.android.AndroidIntegrationTestSetupRule;
+import com.google.idea.blaze.android.sdk.BlazeSdkProvider;
+import com.google.idea.blaze.android.sdk.MockBlazeSdkProvider;
+import com.google.idea.blaze.base.ideinfo.AndroidIdeInfo;
+import com.google.idea.blaze.base.ideinfo.CIdeInfo;
+import com.google.idea.blaze.base.ideinfo.CToolchainIdeInfo;
+import com.google.idea.blaze.base.ideinfo.TargetIdeInfo;
+import com.google.idea.blaze.base.ideinfo.TargetMap;
+import com.google.idea.blaze.base.ideinfo.TargetMapBuilder;
+import com.google.idea.blaze.base.model.BlazeProjectData;
+import com.google.idea.blaze.base.model.primitives.ExecutionRootPath;
+import com.google.idea.blaze.base.model.primitives.Kind;
+import com.google.idea.blaze.base.model.primitives.LanguageClass;
+import com.google.idea.blaze.base.model.primitives.WorkspacePath;
+import com.google.idea.blaze.base.model.primitives.WorkspaceType;
+import com.google.idea.blaze.base.sync.BlazeSyncIntegrationTestCase;
+import com.google.idea.blaze.base.sync.BlazeSyncParams;
+import com.google.idea.blaze.base.sync.BlazeSyncParams.SyncMode;
+import com.google.idea.blaze.base.sync.data.BlazeDataStorage;
+import com.google.idea.blaze.base.sync.data.BlazeProjectDataManager;
+import com.google.idea.blaze.base.sync.projectstructure.ModuleFinder;
+import com.google.idea.blaze.cpp.BlazeCWorkspace;
+import com.google.idea.blaze.java.sync.BlazeJavaSyncAugmenter;
+import com.intellij.openapi.module.Module;
+import com.intellij.openapi.projectRoots.Sdk;
+import com.intellij.openapi.projectRoots.SdkTypeId;
+import com.intellij.openapi.roots.ContentEntry;
+import com.intellij.openapi.vfs.VirtualFile;
+import com.jetbrains.cidr.lang.OCLanguageKind;
+import com.jetbrains.cidr.lang.workspace.OCResolveConfiguration;
+import com.jetbrains.cidr.lang.workspace.OCWorkspace;
+import com.jetbrains.cidr.lang.workspace.OCWorkspaceManager;
+import com.jetbrains.cidr.lang.workspace.compiler.OCCompilerSettings;
+import java.util.Arrays;
+import java.util.List;
+import org.jetbrains.android.facet.AndroidFacet;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** Android-specific sync integration tests. */
+@RunWith(JUnit4.class)
+public class AndroidSyncTest extends BlazeSyncIntegrationTestCase {
+
+  @Rule
+  public final AndroidIntegrationTestSetupRule androidSetupRule =
+      new AndroidIntegrationTestSetupRule();
+
+  @Before
+  public void setup() {
+    mockSdk("android-25", "Android 25 SDK");
+    registerProjectService(OCWorkspaceManager.class, new MockOCWorkspaceManager());
+  }
+
+  private void mockSdk(String targetHash, String sdkName) {
+    SdkTypeId sdkType = mock(SdkTypeId.class);
+    when(sdkType.getName()).thenReturn("Android SDK");
+    Sdk sdk = mock(Sdk.class);
+    when(sdk.getName()).thenReturn(sdkName);
+    when(sdk.getSdkType()).thenReturn(sdkType);
+    MockBlazeSdkProvider sdkProvider = (MockBlazeSdkProvider) BlazeSdkProvider.getInstance();
+    sdkProvider.addSdk(targetHash, sdk);
+  }
+
+  @Test
+  public void testAndroidSyncAugmenterPresent() {
+    assertThat(
+            Arrays.stream(BlazeJavaSyncAugmenter.EP_NAME.getExtensions())
+                .anyMatch(e -> e instanceof BlazeAndroidJavaSyncAugmenter))
+        .isTrue();
+  }
+
+  @Test
+  public void testSimpleSync() throws Exception {
+    setProjectView(
+        "directories:",
+        "  java/com/google",
+        "targets:",
+        "  //java/com/google:lib",
+        "android_sdk_platform: android-25");
+
+    workspace.createFile(
+        new WorkspacePath("java/com/google/Source.java"),
+        "package com.google;",
+        "public class Source {}");
+
+    workspace.createFile(
+        new WorkspacePath("java/com/google/Other.java"),
+        "package com.google;",
+        "public class Other {}");
+
+    VirtualFile javaRoot = workspace.createDirectory(new WorkspacePath("java/com/google"));
+
+    TargetMap targetMap =
+        TargetMapBuilder.builder()
+            .addTarget(
+                TargetIdeInfo.builder()
+                    .setBuildFile(sourceRoot("java/com/google/BUILD"))
+                    .setLabel("//java/com/google:lib")
+                    .setKind("android_library")
+                    .setAndroidInfo(
+                        AndroidIdeInfo.builder()
+                            .setManifestFile(sourceRoot("java/com/google/AndroidManifest.xml"))
+                            .addResource(sourceRoot("java/com/google/res/values/strings.xml"))
+                            .setResourceJavaPackage("com.google")
+                            .setGenerateResourceClass(true))
+                    .addSource(sourceRoot("java/com/google/Source.java"))
+                    .addSource(sourceRoot("java/com/google/Other.java")))
+            .build();
+
+    setTargetMap(targetMap);
+
+    runBlazeSync(
+        new BlazeSyncParams.Builder("Sync", SyncMode.INCREMENTAL)
+            .addProjectViewTargets(true)
+            .build());
+
+    errorCollector.assertNoIssues();
+
+    BlazeProjectData blazeProjectData =
+        BlazeProjectDataManager.getInstance(getProject()).getBlazeProjectData();
+    assertThat(blazeProjectData).isNotNull();
+    assertThat(blazeProjectData.targetMap).isEqualTo(targetMap);
+    assertThat(blazeProjectData.workspaceLanguageSettings.getWorkspaceType())
+        .isEqualTo(WorkspaceType.ANDROID);
+
+    ImmutableList<ContentEntry> contentEntries = getWorkspaceContentEntries();
+    assertThat(contentEntries).hasSize(1);
+    assertThat(findContentEntry(javaRoot)).isNotNull();
+    assertThat(findContentEntry(javaRoot).getSourceFolders()).hasLength(1);
+
+    // Check that the workspace is set to android
+    Module workspaceModule =
+        ModuleFinder.getInstance(getProject())
+            .findModuleByName(BlazeDataStorage.WORKSPACE_MODULE_NAME);
+    assertThat(workspaceModule).isNotNull();
+    assertThat(AndroidFacet.getInstance(workspaceModule)).isNotNull();
+
+    // Check that a resource module was created
+    Module resourceModule =
+        ModuleFinder.getInstance(getProject()).findModuleByName("java.com.google.lib");
+    assertThat(resourceModule).isNotNull();
+    assertThat(AndroidFacet.getInstance(resourceModule)).isNotNull();
+  }
+
+  @Test
+  public void testMultipleToolchainsNoIssue() {
+    // Test what happens if there are multiple toolchains in the target map
+    // (e.g., from --fat_apk_cpu)
+    setProjectView(
+        "directories:",
+        "  java/com/google",
+        "targets:",
+        "  //java/com/google:app",
+        "additional_languages:",
+        "  c",
+        "android_sdk_platform: android-25");
+    workspace.createDirectory(new WorkspacePath("java/com/google"));
+    workspace.createFile(
+        new WorkspacePath("java/com/google/Source.java"),
+        "package com.google;",
+        "public class Source {}");
+
+    workspace.createFile(
+        new WorkspacePath("java/com/google/Other.java"),
+        "package com.google;",
+        "public class Other {}");
+
+    workspace.createFile(new WorkspacePath("java/com/google/jni/native.cc"), "void foo() {}");
+    workspace.createFile(new WorkspacePath("java/com/google/jni/native2.cc"), "void bar() {}");
+
+    TargetMap targetMap =
+        TargetMapBuilder.builder()
+            .addTarget(
+                TargetIdeInfo.builder()
+                    .setBuildFile(sourceRoot("android_ndk_linux/toolchains/BUILD"))
+                    .setLabel("//android_ndk_linux/toolchains:armv7a")
+                    .setKind(Kind.CC_TOOLCHAIN)
+                    .setCToolchainInfo(
+                        CToolchainIdeInfo.builder()
+                            .setTargetName("arm-linux-androideabi")
+                            .setCppExecutable(
+                                new ExecutionRootPath("bin/arm-linux-androideabi-gcc"))
+                            .setPreprocessorExecutable(
+                                new ExecutionRootPath("bin/arm-linux-androideabi-cpp"))
+                            .addBaseCompilerOptions(
+                                ImmutableList.of(
+                                    "-DOS_ANDROID",
+                                    "-mbionic",
+                                    "-ffunction-sections",
+                                    "-march=armv7-a",
+                                    "-mfpu=vfpv3-d16"))
+                            .addCppCompilerOptions(ImmutableList.of("-std=gnu++11"))
+                            .addBuiltInIncludeDirectories(
+                                ImmutableList.of(
+                                    new ExecutionRootPath(
+                                        "lib/gcc/arm-linux-androideabi/4.8/include")))
+                            .addLinkOptions(
+                                ImmutableList.of(
+                                    "--sysroot=android_ndk_linux/platforms/android-18/arch-arm"))
+                            .addUnfilteredCompilerOptions(
+                                ImmutableList.of(
+                                    "--sysroot=android_ndk_linux/platforms/android-18/arch-arm"))
+                            .addUnfilteredToolchainSystemIncludes(
+                                ImmutableList.of(
+                                    new ExecutionRootPath(
+                                        "android_ndk_linux/sources/llvm-libc++/libcxx/include")))))
+            .addTarget(
+                TargetIdeInfo.builder()
+                    .setBuildFile(sourceRoot("android_ndk_linux/toolchains/BUILD"))
+                    .setLabel("//android_ndk_linux/toolchains:aarch64")
+                    .setKind(Kind.CC_TOOLCHAIN)
+                    .setCToolchainInfo(
+                        CToolchainIdeInfo.builder()
+                            .setTargetName("aarch64-linux-android")
+                            .setCppExecutable(
+                                new ExecutionRootPath("prebuilt/bin/aarch64-linux-android-gcc"))
+                            .setPreprocessorExecutable(
+                                new ExecutionRootPath("prebuilt/bin/aarch64-linux-android-cpp"))
+                            .addBaseCompilerOptions(
+                                ImmutableList.of("-DOS_ANDROID", "-mbionic", "-ffunction-sections"))
+                            .addCppCompilerOptions(ImmutableList.of("-std=gnu++11"))
+                            .addBuiltInIncludeDirectories(
+                                ImmutableList.of(
+                                    new ExecutionRootPath(
+                                        "lib/gcc/aarch64-linux-android/4.9/include")))
+                            .addLinkOptions(
+                                ImmutableList.of(
+                                    "--sysroot=android_ndk_linux/platforms/android-21/arch-arm64"))
+                            .addUnfilteredCompilerOptions(
+                                ImmutableList.of(
+                                    "--sysroot=android_ndk_linux/platforms/android-21/arch-arm64"))
+                            .addUnfilteredToolchainSystemIncludes(
+                                ImmutableList.of(
+                                    new ExecutionRootPath(
+                                        "android_ndk_linux/sources/llvm-libc++/libcxx/include")))))
+            .addTarget(
+                TargetIdeInfo.builder()
+                    .setBuildFile(sourceRoot("java/com/google/BUILD"))
+                    .setLabel("//java/com/google:lib")
+                    .setKind("android_library")
+                    .setAndroidInfo(
+                        AndroidIdeInfo.builder()
+                            .setManifestFile(sourceRoot("java/com/google/AndroidManifest.xml"))
+                            .addResource(sourceRoot("java/com/google/res/values/strings.xml"))
+                            .setResourceJavaPackage("com.google")
+                            .setGenerateResourceClass(true))
+                    .addSource(sourceRoot("java/com/google/Other.java")))
+            // Technically, blaze returns multiple instances of native libs (one for each CPU from
+            // fat APK). However, we just pick the first instance we run into for the target map.
+            .addTarget(
+                TargetIdeInfo.builder()
+                    .setBuildFile(sourceRoot("java/com/google/BUILD"))
+                    .setLabel("//java/com/google:native_lib")
+                    .setKind("cc_library")
+                    .setCInfo(
+                        CIdeInfo.builder()
+                            .addTransitiveQuoteIncludeDirectories(
+                                ImmutableList.of(
+                                    new ExecutionRootPath("."),
+                                    new ExecutionRootPath("blaze-out/android-aarch64-etc/genfiles"),
+                                    new ExecutionRootPath(
+                                        "blaze-out/android-aarch64-etc/genfiles/third_party/java")))
+                            .addTransitiveSystemIncludeDirectories(
+                                ImmutableList.of(
+                                    new ExecutionRootPath("third_party/stl/gcc3"),
+                                    new ExecutionRootPath("third_party/java/jdk/include"))))
+                    .addSource(sourceRoot("java/com/google/jni/native.cc"))
+                    .addDependency("//android_ndk_linux/toolchains:aarch64"))
+            .addTarget(
+                TargetIdeInfo.builder()
+                    .setBuildFile(sourceRoot("java/com/google/BUILD"))
+                    .setLabel("//java/com/google:native_lib2")
+                    .setKind("cc_library")
+                    .setCInfo(
+                        CIdeInfo.builder()
+                            .addTransitiveQuoteIncludeDirectories(
+                                ImmutableList.of(
+                                    new ExecutionRootPath("."),
+                                    new ExecutionRootPath("blaze-out/android-aarch64-etc/genfiles"),
+                                    new ExecutionRootPath(
+                                        "blaze-out/android-aarch64-etc/genfiles/third_party/java")))
+                            .addTransitiveSystemIncludeDirectories(
+                                ImmutableList.of(
+                                    new ExecutionRootPath("third_party/stl/gcc3"),
+                                    new ExecutionRootPath("third_party/java/jdk/include"))))
+                    .addSource(sourceRoot("java/com/google/jni/native2.cc"))
+                    .addDependency("//java/com/google:native_lib")
+                    .addDependency("//android_ndk_linux/toolchains:armv7a"))
+            // Other targets like android_binary and android_test might also depend on
+            // a cc_toolchain.
+            .addTarget(
+                TargetIdeInfo.builder()
+                    .setBuildFile(sourceRoot("java/com/google/BUILD"))
+                    .setLabel("//java/com/google:app")
+                    .setKind("android_binary")
+                    .setAndroidInfo(
+                        AndroidIdeInfo.builder()
+                            .setManifestFile(sourceRoot("java/com/google/AndroidManifest.xml"))
+                            .setResourceJavaPackage("com.google")
+                            .setGenerateResourceClass(true))
+                    .addSource(sourceRoot("java/com/google/Source.java"))
+                    .addDependency("//tools/jdk:toolchain")
+                    .addDependency("//android_ndk_linux/toolchains:armv7a")
+                    .addDependency("//java/com/google:lib")
+                    .addDependency("//java/com/google:native_lib")
+                    .addDependency("//java/com/google:native_lib2"))
+            .build();
+
+    setTargetMap(targetMap);
+
+    runBlazeSync(
+        new BlazeSyncParams.Builder("Sync", SyncMode.INCREMENTAL)
+            .addProjectViewTargets(true)
+            .build());
+
+    errorCollector.assertNoIssues();
+
+    BlazeProjectData blazeProjectData =
+        BlazeProjectDataManager.getInstance(getProject()).getBlazeProjectData();
+    assertThat(blazeProjectData).isNotNull();
+    assertThat(blazeProjectData.targetMap).isEqualTo(targetMap);
+    assertThat(blazeProjectData.workspaceLanguageSettings.getWorkspaceType())
+        .isEqualTo(WorkspaceType.ANDROID);
+    assertThat(blazeProjectData.workspaceLanguageSettings.isLanguageActive(LanguageClass.C))
+        .isTrue();
+
+    // Check that the workspace is set to android
+    Module workspaceModule =
+        ModuleFinder.getInstance(getProject())
+            .findModuleByName(BlazeDataStorage.WORKSPACE_MODULE_NAME);
+    assertThat(workspaceModule).isNotNull();
+    assertThat(AndroidFacet.getInstance(workspaceModule)).isNotNull();
+
+    // Check resolve configurations for the native code match the toolchain that was in
+    // the library's deps (not switched for some reason).
+    VirtualFile nativeCc =
+        fileSystem.findFile(
+            workspaceRoot
+                .fileForPath(new WorkspacePath("java/com/google/jni/native.cc"))
+                .getPath());
+    VirtualFile nativeCc2 =
+        fileSystem.findFile(
+            workspaceRoot
+                .fileForPath(new WorkspacePath("java/com/google/jni/native2.cc"))
+                .getPath());
+
+    List<? extends OCResolveConfiguration> resolveConfigurations =
+        OCWorkspaceManager.getWorkspace(getProject()).getConfigurationsForFile(nativeCc);
+    assertThat(resolveConfigurations).hasSize(1);
+    OCCompilerSettings compilerSettings = resolveConfigurations.get(0).getCompilerSettings();
+    List<String> compilerSwitches =
+        compilerSettings.getCompilerSwitches(OCLanguageKind.CPP, nativeCc).getCommandLineArgs();
+    assertThat(compilerSwitches)
+        .contains("--sysroot=android_ndk_linux/platforms/android-21/arch-arm64");
+
+    resolveConfigurations =
+        OCWorkspaceManager.getWorkspace(getProject()).getConfigurationsForFile(nativeCc2);
+    assertThat(resolveConfigurations).hasSize(1);
+    compilerSettings = resolveConfigurations.get(0).getCompilerSettings();
+    compilerSwitches =
+        compilerSettings.getCompilerSwitches(OCLanguageKind.CPP, nativeCc).getCommandLineArgs();
+    assertThat(compilerSwitches)
+        .contains("--sysroot=android_ndk_linux/platforms/android-18/arch-arm");
+  }
+
+  private class MockOCWorkspaceManager extends OCWorkspaceManager {
+
+    @Override
+    public OCWorkspace getWorkspace() {
+      return BlazeCWorkspace.getInstance(getProject());
+    }
+  }
+}
diff --git a/aswb/2.3/tests/integrationtests/com/google/idea/blaze/android/sync/model/idea/BlazeClassJarProviderIntegrationTest.java b/aswb/tests/integrationtests/com/google/idea/blaze/android/sync/model/idea/BlazeClassJarProviderIntegrationTest.java
similarity index 94%
rename from aswb/2.3/tests/integrationtests/com/google/idea/blaze/android/sync/model/idea/BlazeClassJarProviderIntegrationTest.java
rename to aswb/tests/integrationtests/com/google/idea/blaze/android/sync/model/idea/BlazeClassJarProviderIntegrationTest.java
index 0be0dc0..e784f6a 100644
--- a/aswb/2.3/tests/integrationtests/com/google/idea/blaze/android/sync/model/idea/BlazeClassJarProviderIntegrationTest.java
+++ b/aswb/tests/integrationtests/com/google/idea/blaze/android/sync/model/idea/BlazeClassJarProviderIntegrationTest.java
@@ -283,20 +283,21 @@
   }
 
   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");
+    Label mainResourceLibrary = Label.create("//com/google/example:main");
+    Label androidLibraryDependency = Label.create("//com/google/example:android_lib");
+    Label androidResourceDependency = Label.create("//com/google/example:android_res");
+    Label androidResourceDependency2 = Label.create("//com/google/example:android_res2");
+    Label transitiveResourceDependency =
+        Label.create("//com/google/example/transitive:android_res");
+    Label javaDependency = Label.create("//com/google/example:java");
+    Label transitiveJavaDependency = Label.create("//com/google/example/transitive:java");
+    Label sharedJavaDependency = Label.create("//com/google/example/shared:java");
+    Label sharedJavaDependency2 = Label.create("//com/google/example/shared2:java");
+    Label importDependency = Label.create("//com/google/example:import");
+    Label transitiveImportDependency = Label.create("//com/google/example/transitive:import");
+    Label unrelatedJava = Label.create("//com/google/unrelated:java");
+    Label unrelatedAndroidLibrary = Label.create("//com/google/unrelated:android_lib");
+    Label unrelatedAndroidResource = Label.create("//com/google/unrelated:android_res");
 
     AndroidResourceModuleRegistry registry = new AndroidResourceModuleRegistry();
     registry.put(
diff --git a/aswb/2.3/tests/unittests/com/google/idea/blaze/android/project/BlazeBuildSystemServiceTest.java b/aswb/tests/unittests/com/google/idea/blaze/android/project/BlazeBuildSystemServiceTest.java
similarity index 96%
rename from aswb/2.3/tests/unittests/com/google/idea/blaze/android/project/BlazeBuildSystemServiceTest.java
rename to aswb/tests/unittests/com/google/idea/blaze/android/project/BlazeBuildSystemServiceTest.java
index fbd398d..283773a 100755
--- a/aswb/2.3/tests/unittests/com/google/idea/blaze/android/project/BlazeBuildSystemServiceTest.java
+++ b/aswb/tests/unittests/com/google/idea/blaze/android/project/BlazeBuildSystemServiceTest.java
@@ -122,7 +122,7 @@
     PsiFile psiFile = mock(PsiFile.class);
 
     BuildReferenceManager buildReferenceManager = BuildReferenceManager.getInstance(project);
-    when(buildReferenceManager.resolveLabel(new Label("//foo:bar"))).thenReturn(buildTargetPsi);
+    when(buildReferenceManager.resolveLabel(Label.create("//foo:bar"))).thenReturn(buildTargetPsi);
     when(buildTargetPsi.getContainingFile()).thenReturn(psiFile);
     when(buildTargetPsi.getTextOffset()).thenReturn(1337);
 
@@ -149,7 +149,7 @@
   @Test
   public void testAddDependencyWithoutBuildTargetPsi() throws Exception {
     // Can't find PSI for the target.
-    when(BuildReferenceManager.getInstance(project).resolveLabel(new Label("//foo:bar")))
+    when(BuildReferenceManager.getInstance(project).resolveLabel(Label.create("//foo:bar")))
         .thenReturn(null);
 
     VirtualFile buildFile =
@@ -165,9 +165,9 @@
   }
 
   private void mockBlazeImportSettings(Container projectServices) {
-    BlazeImportSettingsManager importSettingsManager = new BlazeImportSettingsManager(project);
+    BlazeImportSettingsManager importSettingsManager = new BlazeImportSettingsManager();
     importSettingsManager.setImportSettings(
-        new BlazeImportSettings("", "", "", "", "", Blaze.BuildSystem.Blaze));
+        new BlazeImportSettings("", "", "", "", Blaze.BuildSystem.Blaze));
     projectServices.register(BlazeImportSettingsManager.class, importSettingsManager);
   }
 
@@ -195,7 +195,7 @@
     AndroidResourceModuleRegistry moduleRegistry = new AndroidResourceModuleRegistry();
     moduleRegistry.put(
         module,
-        AndroidResourceModule.builder(TargetKey.forPlainTarget(new Label("//foo:bar"))).build());
+        AndroidResourceModule.builder(TargetKey.forPlainTarget(Label.create("//foo:bar"))).build());
     projectServices.register(AndroidResourceModuleRegistry.class, moduleRegistry);
   }
 
@@ -204,7 +204,7 @@
         TargetMapBuilder.builder()
             .addTarget(
                 TargetIdeInfo.builder()
-                    .setLabel(new Label("//foo:bar"))
+                    .setLabel(Label.create("//foo:bar"))
                     .setBuildFile(ArtifactLocation.builder().setRelativePath("foo/BUILD").build())
                     .build())
             .build();
diff --git a/aswb/2.3/tests/unittests/com/google/idea/blaze/android/project/BlazeFeatureEnabledServiceTest.java b/aswb/tests/unittests/com/google/idea/blaze/android/project/BlazeFeatureEnabledServiceTest.java
similarity index 94%
rename from aswb/2.3/tests/unittests/com/google/idea/blaze/android/project/BlazeFeatureEnabledServiceTest.java
rename to aswb/tests/unittests/com/google/idea/blaze/android/project/BlazeFeatureEnabledServiceTest.java
index efd77ea..18e44ee 100644
--- a/aswb/2.3/tests/unittests/com/google/idea/blaze/android/project/BlazeFeatureEnabledServiceTest.java
+++ b/aswb/tests/unittests/com/google/idea/blaze/android/project/BlazeFeatureEnabledServiceTest.java
@@ -23,11 +23,11 @@
 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.logging.EventLogger;
 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.data.BlazeProjectDataManager;
 import com.google.idea.common.experiments.ExperimentService;
 import com.intellij.openapi.extensions.ExtensionPoint;
@@ -59,13 +59,13 @@
     projectDataManager = new MockBlazeProjectDataManager();
     projectServices.register(BlazeProjectDataManager.class, projectDataManager);
 
-    BlazeImportSettingsManager importSettingsManager = new BlazeImportSettingsManager(project);
+    BlazeImportSettingsManager importSettingsManager = new BlazeImportSettingsManager();
     importSettingsManager.setImportSettings(
-        new BlazeImportSettings(null, null, null, null, null, BuildSystem.Blaze));
+        new BlazeImportSettings("", "", "", "", BuildSystem.Blaze));
     projectServices.register(BlazeImportSettingsManager.class, importSettingsManager);
-    projectServices.register(
-        BlazeImportSettingsManagerLegacy.class, new BlazeImportSettingsManagerLegacy(project));
 
+    registerExtensionPoint(
+        ExtensionPointName.create("com.google.idea.blaze.EventLogger"), EventLogger.class);
     ExtensionPoint<FeatureEnableService> extensionPoint =
         registerExtensionPoint(
             ExtensionPointName.create("com.android.project.featureEnableService"),
diff --git a/aswb/2.3/tests/unittests/com/google/idea/blaze/android/rendering/BlazeRenderErrorContributorTest.java b/aswb/tests/unittests/com/google/idea/blaze/android/rendering/BlazeRenderErrorContributorTest.java
similarity index 94%
rename from aswb/2.3/tests/unittests/com/google/idea/blaze/android/rendering/BlazeRenderErrorContributorTest.java
rename to aswb/tests/unittests/com/google/idea/blaze/android/rendering/BlazeRenderErrorContributorTest.java
index ffe8267..2b49d8c 100644
--- a/aswb/2.3/tests/unittests/com/google/idea/blaze/android/rendering/BlazeRenderErrorContributorTest.java
+++ b/aswb/tests/unittests/com/google/idea/blaze/android/rendering/BlazeRenderErrorContributorTest.java
@@ -44,7 +44,6 @@
 import com.google.idea.blaze.base.settings.Blaze.BuildSystem;
 import com.google.idea.blaze.base.settings.BlazeImportSettings;
 import com.google.idea.blaze.base.settings.BlazeImportSettingsManager;
-import com.google.idea.blaze.base.settings.BlazeImportSettingsManagerLegacy;
 import com.google.idea.blaze.base.sync.data.BlazeProjectDataManager;
 import com.google.idea.blaze.base.sync.workspace.ArtifactLocationDecoder;
 import com.google.idea.blaze.base.targetmaps.SourceToTargetMap;
@@ -103,12 +102,10 @@
     projectServices.register(
         AndroidResourceModuleRegistry.class, new AndroidResourceModuleRegistry());
 
-    BlazeImportSettingsManager importSettingsManager = new BlazeImportSettingsManager(project);
-    BlazeImportSettings settings = new BlazeImportSettings("", "", "", "", "", BuildSystem.Blaze);
+    BlazeImportSettingsManager importSettingsManager = new BlazeImportSettingsManager();
+    BlazeImportSettings settings = new BlazeImportSettings("", "", "", "", BuildSystem.Blaze);
     importSettingsManager.setImportSettings(settings);
     projectServices.register(BlazeImportSettingsManager.class, importSettingsManager);
-    projectServices.register(
-        BlazeImportSettingsManagerLegacy.class, new BlazeImportSettingsManagerLegacy(project));
 
     createPsiClassesAndSourceToTargetMap(projectServices);
 
@@ -345,14 +342,14 @@
   }
 
   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 mainResourcesTarget = Label.create("//com/google/example:main");
+    Label dependencyGeneratedResourceTarget = Label.create("//com/google/example:generated");
+    Label dependencySourceResourceTarget = Label.create("//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");
+        Label.create("//com/google/example/transitive:generated");
+    Label transitiveSourceResourceTarget = Label.create("//com/google/example/transitive:source");
+    Label unrelatedGeneratedResourceTarget = Label.create("//com/google/unrelated:generated");
+    Label unrelatedSourceResourceTarget = Label.create("//com/google/unrelated:source");
 
     ArtifactLocation mainGeneratedResource =
         artifact("com/google/example/main/generated/res", false);
@@ -500,7 +497,7 @@
   }
 
   private void createTargetMapWithNonStandardAndroidManifestName() {
-    Label mainResourceTarget = new Label("//com/google/example:main");
+    Label mainResourceTarget = Label.create("//com/google/example:main");
 
     ArtifactLocation mainManifest = artifact("com/google/example/main/WeirdManifest.xml", true);
     ArtifactLocation mainResource = artifact("com/google/example/main/res", true);
@@ -530,8 +527,8 @@
   }
 
   private void createTargetMapWithNonStandardAndroidManifestNameInDependency() {
-    Label mainResourceTarget = new Label("//com/google/example:main");
-    Label dependencyResourceTarget = new Label("//com/google/example:dependency");
+    Label mainResourceTarget = Label.create("//com/google/example:main");
+    Label dependencyResourceTarget = Label.create("//com/google/example:dependency");
 
     ArtifactLocation mainManifest = artifact("com/google/example/main/AndroidManifest.xml", true);
     ArtifactLocation mainResource = artifact("com/google/example/main/res", true);
@@ -582,11 +579,11 @@
   }
 
   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");
+    Label parentTarget = Label.create("//com/google/example:app");
+    Label independentLibraryTarget = Label.create("//com/google/example/independent:library");
+    Label independentLibrary2Target = Label.create("//com/google/example/independent:library2");
+    Label dependentLibraryTarget = Label.create("//com/google/example/dependent:library");
+    Label resourcesTarget = Label.create("//com/google/example:resources");
 
     ArtifactLocation manifest = artifact("com/google/example/AndroidManifest.xml", true);
     ArtifactLocation resources = artifact("com/google/example/res", true);
@@ -667,15 +664,15 @@
     ImmutableMap<File, TargetKey> sourceToTarget =
         ImmutableMap.of(
             VfsUtilCore.virtualToIoFile(independentLibraryView),
-            TargetKey.forPlainTarget(new Label("//com/google/example/independent:library")),
+            TargetKey.forPlainTarget(Label.create("//com/google/example/independent:library")),
             VfsUtilCore.virtualToIoFile(independentLibraryView2),
-            TargetKey.forPlainTarget(new Label("//com/google/example/independent:library")),
+            TargetKey.forPlainTarget(Label.create("//com/google/example/independent:library")),
             VfsUtilCore.virtualToIoFile(independentLibrary2View),
-            TargetKey.forPlainTarget(new Label("//com/google/example/independent:library2")),
+            TargetKey.forPlainTarget(Label.create("//com/google/example/independent:library2")),
             VfsUtilCore.virtualToIoFile(dependentLibraryView),
-            TargetKey.forPlainTarget(new Label("//com/google/example/dependent:library")),
+            TargetKey.forPlainTarget(Label.create("//com/google/example/dependent:library")),
             VfsUtilCore.virtualToIoFile(resourceView),
-            TargetKey.forPlainTarget(new Label("//com/google/example:resources")));
+            TargetKey.forPlainTarget(Label.create("//com/google/example:resources")));
 
     projectServices.register(
         JavaPsiFacade.class, new MockJavaPsiFacade(project, psiManager, classes));
diff --git a/aswb/2.3/tests/unittests/com/google/idea/blaze/android/run/testrecorder/BlazeConfigurationsTest.java b/aswb/tests/unittests/com/google/idea/blaze/android/run/testrecorder/BlazeConfigurationsTest.java
similarity index 91%
rename from aswb/2.3/tests/unittests/com/google/idea/blaze/android/run/testrecorder/BlazeConfigurationsTest.java
rename to aswb/tests/unittests/com/google/idea/blaze/android/run/testrecorder/BlazeConfigurationsTest.java
index 86c20cf..b48fe02 100644
--- a/aswb/2.3/tests/unittests/com/google/idea/blaze/android/run/testrecorder/BlazeConfigurationsTest.java
+++ b/aswb/tests/unittests/com/google/idea/blaze/android/run/testrecorder/BlazeConfigurationsTest.java
@@ -23,7 +23,6 @@
 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;
@@ -33,6 +32,7 @@
 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.command.info.BlazeInfo;
 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;
@@ -151,7 +151,7 @@
         BlazeCommandRunConfigurationType.getInstance()
             .getFactory()
             .createTemplateConfiguration(project);
-    blazeConfiguration.setTarget(new Label("//label:android_binary_rule"));
+    blazeConfiguration.setTarget(Label.create("//label:android_binary_rule"));
     BlazeAndroidBinaryRunConfigurationState configurationState =
         ((BlazeAndroidBinaryRunConfigurationHandler) blazeConfiguration.getHandler()).getState();
     configurationState.setMode(BlazeAndroidBinaryRunConfigurationState.LAUNCH_SPECIFIC_ACTIVITY);
@@ -164,9 +164,9 @@
   }
 
   private void mockBlazeImportSettings(Container projectServices) {
-    BlazeImportSettingsManager importSettingsManager = new BlazeImportSettingsManager(project);
+    BlazeImportSettingsManager importSettingsManager = new BlazeImportSettingsManager();
     importSettingsManager.setImportSettings(
-        new BlazeImportSettings("", "", "", "", "", Blaze.BuildSystem.Blaze));
+        new BlazeImportSettings("", "", "", "", Blaze.BuildSystem.Blaze));
     projectServices.register(BlazeImportSettingsManager.class, importSettingsManager);
   }
 
@@ -178,12 +178,12 @@
     BlazeCommandRunConfiguration blazeAndroidBinaryConfiguration =
         configurationFactory.createTemplateConfiguration(project);
     blazeAndroidBinaryConfiguration.setName("AndroidBinaryConfiguration");
-    blazeAndroidBinaryConfiguration.setTarget(new Label("//label:android_binary_rule"));
+    blazeAndroidBinaryConfiguration.setTarget(Label.create("//label:android_binary_rule"));
 
     BlazeCommandRunConfiguration blazeAndroidTestConfiguration =
         configurationFactory.createTemplateConfiguration(project);
     blazeAndroidTestConfiguration.setName("AndroidTestConfiguration");
-    blazeAndroidTestConfiguration.setTarget(new Label("//label:android_test_rule"));
+    blazeAndroidTestConfiguration.setTarget(Label.create("//label:android_test_rule"));
 
     runManager.addConfiguration(
         runManager.createConfiguration(blazeAndroidBinaryConfiguration, configurationFactory),
@@ -192,7 +192,7 @@
         runManager.createConfiguration(blazeAndroidTestConfiguration, configurationFactory), true);
   }
 
-  private class MockTargetFinder extends TargetFinder {
+  private static class MockTargetFinder extends TargetFinder {
     @Override
     public List<TargetIdeInfo> findTargets(Project project, Predicate<TargetIdeInfo> predicate) {
       return null;
@@ -201,9 +201,9 @@
     @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"))) {
+      if (label.equals(Label.create("//label:android_binary_rule"))) {
         builder.setKind(Kind.ANDROID_BINARY);
-      } else if (label.equals(new Label("//label:android_test_rule"))) {
+      } else if (label.equals(Label.create("//label:android_test_rule"))) {
         builder.setKind(Kind.ANDROID_TEST);
       }
       return builder.build();
@@ -236,7 +236,7 @@
     @Override
     public Module getModule() {
       Label label = getLabel();
-      if (label != null && label.equals(new Label("//label:android_binary_rule"))) {
+      if (label != null && label.equals(Label.create("//label:android_binary_rule"))) {
         return mockModule;
       }
 
@@ -244,13 +244,18 @@
     }
   }
 
-  private class MockBuildSystemProvider implements BuildSystemProvider {
+  private static class MockBuildSystemProvider implements BuildSystemProvider {
     @Override
     public Blaze.BuildSystem buildSystem() {
       return Blaze.BuildSystem.Blaze;
     }
 
     @Override
+    public String getBinaryPath() {
+      return "/usr/bin/blaze";
+    }
+
+    @Override
     public WorkspaceRootProvider getWorkspaceRootProvider() {
       return null;
     }
@@ -266,6 +271,12 @@
       return null;
     }
 
+    @Nullable
+    @Override
+    public String getProjectViewDocumentationUrl() {
+      return null;
+    }
+
     @Override
     public boolean isBuildFile(String fileName) {
       return false;
@@ -278,15 +289,15 @@
     }
 
     @Override
-    public FileNameMatcher buildFileMatcher() {
-      return null;
+    public ImmutableList<FileNameMatcher> buildLanguageFileTypeMatchers() {
+      return ImmutableList.of();
     }
 
     @Override
     public void populateBlazeVersionData(
         BuildSystem buildSystem,
         WorkspaceRoot workspaceRoot,
-        ImmutableMap<String, String> blazeInfo,
+        BlazeInfo blazeInfo,
         BlazeVersionData.Builder builder) {}
   }
 }
diff --git a/aswb/tests/unittests/com/google/idea/blaze/android/sync/importer/BlazeAndroidWorkspaceImporterTest.java b/aswb/tests/unittests/com/google/idea/blaze/android/sync/importer/BlazeAndroidWorkspaceImporterTest.java
index c839293..e720a87 100644
--- a/aswb/tests/unittests/com/google/idea/blaze/android/sync/importer/BlazeAndroidWorkspaceImporterTest.java
+++ b/aswb/tests/unittests/com/google/idea/blaze/android/sync/importer/BlazeAndroidWorkspaceImporterTest.java
@@ -81,7 +81,7 @@
           artifactLocation -> new File("/", artifactLocation.getRelativePath());
 
   private static final BlazeImportSettings DUMMY_IMPORT_SETTINGS =
-      new BlazeImportSettings("", "", "", "", "", BuildSystem.Blaze);
+      new BlazeImportSettings("", "", "", "", BuildSystem.Blaze);
 
   private BlazeContext context;
   private ErrorCollector errorCollector = new ErrorCollector();
@@ -95,8 +95,7 @@
     BlazeExecutor blazeExecutor = new MockBlazeExecutor();
     applicationServices.register(BlazeExecutor.class, blazeExecutor);
 
-    projectServices.register(
-        BlazeImportSettingsManager.class, new BlazeImportSettingsManager(project));
+    projectServices.register(BlazeImportSettingsManager.class, new BlazeImportSettingsManager());
     BlazeImportSettingsManager.getInstance(getProject()).setImportSettings(DUMMY_IMPORT_SETTINGS);
 
     MockFileAttributeProvider mockFileAttributeProvider = new MockFileAttributeProvider();
@@ -221,7 +220,7 @@
     assertThat(result.androidResourceModules)
         .containsExactly(
             AndroidResourceModule.builder(
-                    TargetKey.forPlainTarget(new Label("//java/apps/example:example_debug")))
+                    TargetKey.forPlainTarget(Label.create("//java/apps/example:example_debug")))
                 .addResourceAndTransitiveResource(source("java/apps/example/res"))
                 .addTransitiveResource(source("java/apps/example/lib0/res"))
                 .addTransitiveResource(source("java/apps/example/lib1/res"))
@@ -231,7 +230,7 @@
                 .addTransitiveResourceDependency("//java/libraries/shared:shared")
                 .build(),
             AndroidResourceModule.builder(
-                    TargetKey.forPlainTarget(new Label("//java/apps/example/lib0:lib0")))
+                    TargetKey.forPlainTarget(Label.create("//java/apps/example/lib0:lib0")))
                 .addResourceAndTransitiveResource(source("java/apps/example/lib0/res"))
                 .addTransitiveResource(source("java/apps/example/lib1/res"))
                 .addTransitiveResource(source("java/libraries/shared/res"))
@@ -239,7 +238,7 @@
                 .addTransitiveResourceDependency("//java/libraries/shared:shared")
                 .build(),
             AndroidResourceModule.builder(
-                    TargetKey.forPlainTarget(new Label("//java/apps/example/lib1:lib1")))
+                    TargetKey.forPlainTarget(Label.create("//java/apps/example/lib1:lib1")))
                 .addResourceAndTransitiveResource(source("java/apps/example/lib1/res"))
                 .addTransitiveResource(source("java/libraries/shared/res"))
                 .addTransitiveResourceDependency("//java/libraries/shared:shared")
@@ -468,7 +467,7 @@
                     .setKind("android_library")
                     .setAndroidInfo(
                         AndroidIdeInfo.builder()
-                            .setLegacyResources(new Label("//java/example:resources"))
+                            .setLegacyResources(Label.create("//java/example:resources"))
                             .setManifestFile(source("java/example/AndroidManifest.xml"))
                             .addResource(source("java/example/res"))
                             .setGenerateResourceClass(true)
@@ -492,7 +491,7 @@
     assertThat(result.androidResourceModules)
         .containsExactly(
             AndroidResourceModule.builder(
-                    TargetKey.forPlainTarget(new Label("//java/example:resources")))
+                    TargetKey.forPlainTarget(Label.create("//java/example:resources")))
                 .addResourceAndTransitiveResource(source("java/example/res"))
                 .build());
   }
@@ -588,7 +587,8 @@
 
     assertThat(result.androidResourceModules)
         .containsExactly(
-            AndroidResourceModule.builder(TargetKey.forPlainTarget(new Label("//java/example:lib")))
+            AndroidResourceModule.builder(
+                    TargetKey.forPlainTarget(Label.create("//java/example:lib")))
                 .addResourceAndTransitiveResource(source("java/example/res"))
                 .build());
   }
@@ -654,7 +654,8 @@
     errorCollector.assertNoIssues();
     assertThat(result.androidResourceModules)
         .containsExactly(
-            AndroidResourceModule.builder(TargetKey.forPlainTarget(new Label("//java/example:lib")))
+            AndroidResourceModule.builder(
+                    TargetKey.forPlainTarget(Label.create("//java/example:lib")))
                 .addResourceAndTransitiveResource(source("java/example/res"))
                 .addResourceAndTransitiveResource(gen("java/example/res"))
                 .build());
@@ -759,7 +760,7 @@
     assertThat(result.androidResourceModules)
         .containsExactly(
             AndroidResourceModule.builder(
-                    TargetKey.forPlainTarget(new Label("//java/uninterestingdir:lib")))
+                    TargetKey.forPlainTarget(Label.create("//java/uninterestingdir:lib")))
                 .addResourceAndTransitiveResource(source("java/uninterestingdir/res"))
                 .build());
   }
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
index 9a8e66c..f21c480 100644
--- 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
@@ -51,11 +51,13 @@
     Module moduleTwo = mock(Module.class);
     Module moduleThree = mock(Module.class);
     AndroidResourceModule resourceModuleOne =
-        AndroidResourceModule.builder(TargetKey.forPlainTarget(new Label("//foo/bar:one"))).build();
+        AndroidResourceModule.builder(TargetKey.forPlainTarget(Label.create("//foo/bar:one")))
+            .build();
     AndroidResourceModule resourceModuleTwo =
-        AndroidResourceModule.builder(TargetKey.forPlainTarget(new Label("//foo/bar:two"))).build();
+        AndroidResourceModule.builder(TargetKey.forPlainTarget(Label.create("//foo/bar:two")))
+            .build();
     AndroidResourceModule resourceModuleThree =
-        AndroidResourceModule.builder(TargetKey.forPlainTarget(new Label("//foo/bar:three")))
+        AndroidResourceModule.builder(TargetKey.forPlainTarget(Label.create("//foo/bar:three")))
             .build();
     registry.put(moduleOne, resourceModuleOne);
     registry.put(moduleTwo, resourceModuleTwo);
@@ -76,9 +78,11 @@
   public void testPutSameKeyDifferentValues() {
     Module module = mock(Module.class);
     AndroidResourceModule resourceModuleOne =
-        AndroidResourceModule.builder(TargetKey.forPlainTarget(new Label("//foo/bar:one"))).build();
+        AndroidResourceModule.builder(TargetKey.forPlainTarget(Label.create("//foo/bar:one")))
+            .build();
     AndroidResourceModule resourceModuleTwo =
-        AndroidResourceModule.builder(TargetKey.forPlainTarget(new Label("//foo/bar:two"))).build();
+        AndroidResourceModule.builder(TargetKey.forPlainTarget(Label.create("//foo/bar:two")))
+            .build();
     registry.put(module, resourceModuleOne);
     registry.put(module, resourceModuleTwo);
     assertThat(registry.get(module)).isEqualTo(resourceModuleTwo);
@@ -89,7 +93,8 @@
     Module moduleOne = mock(Module.class);
     Module moduleTwo = mock(Module.class);
     AndroidResourceModule resourceModule =
-        AndroidResourceModule.builder(TargetKey.forPlainTarget(new Label("//foo/bar:one"))).build();
+        AndroidResourceModule.builder(TargetKey.forPlainTarget(Label.create("//foo/bar:one")))
+            .build();
     registry.put(moduleOne, resourceModule);
     try {
       registry.put(moduleTwo, resourceModule);
diff --git a/aswb/2.3/tests/utils/integration/com/google/idea/blaze/android/BlazeAndroidIntegrationTestCase.java b/aswb/tests/utils/integration/com/google/idea/blaze/android/AndroidIntegrationTestSetupRule.java
similarity index 68%
rename from aswb/2.3/tests/utils/integration/com/google/idea/blaze/android/BlazeAndroidIntegrationTestCase.java
rename to aswb/tests/utils/integration/com/google/idea/blaze/android/AndroidIntegrationTestSetupRule.java
index 74459a0..088e953 100644
--- a/aswb/2.3/tests/utils/integration/com/google/idea/blaze/android/BlazeAndroidIntegrationTestCase.java
+++ b/aswb/tests/utils/integration/com/google/idea/blaze/android/AndroidIntegrationTestSetupRule.java
@@ -13,10 +13,15 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-
 package com.google.idea.blaze.android;
 
-import com.google.idea.blaze.base.BlazeIntegrationTestCase;
+import org.junit.rules.ExternalResource;
 
-/** Compatibility test class for Blaze Android integration tests. */
-public abstract class BlazeAndroidIntegrationTestCase extends BlazeIntegrationTestCase {}
+/** Runs before Android Studio integration tests. */
+public class AndroidIntegrationTestSetupRule extends ExternalResource {
+
+  @Override
+  protected void before() throws Throwable {
+    System.setProperty("android.studio.sdk.manager.disabled", "true");
+  }
+}
diff --git a/base/BUILD b/base/BUILD
index 850f1dd..7afe714 100644
--- a/base/BUILD
+++ b/base/BUILD
@@ -11,7 +11,7 @@
         "//common/experiments",
         "//common/formatter",
         "//intellij_platform_sdk:plugin_api",
-        "//proto_deps",
+        "//proto:proto_deps",
         "//sdkcompat",
         "@jsr305_annotations//jar",
     ],
@@ -46,7 +46,7 @@
         ":unit_test_utils",
         "//base",
         "//intellij_platform_sdk:plugin_api_for_tests",
-        "//proto_deps",
+        "//proto:proto_deps",
         "//testing:lib",
         "@jsr305_annotations//jar",
         "@junit//jar",
@@ -91,7 +91,7 @@
         "//common/experiments",
         "//common/experiments:unit_test_utils",
         "//intellij_platform_sdk:plugin_api_for_tests",
-        "//proto_deps",
+        "//proto:proto_deps",
         "@jsr305_annotations//jar",
         "@junit//jar",
     ],
@@ -110,7 +110,7 @@
         ":integration_test_utils",
         ":unit_test_utils",
         "//intellij_platform_sdk:plugin_api_for_tests",
-        "//proto_deps",
+        "//proto:proto_deps",
         "@jsr305_annotations//jar",
         "@junit//jar",
     ],
diff --git a/base/resources/binaries/bazel-buildifier b/base/resources/binaries/bazel-buildifier
index c7347bb..9b5f6b0 100755
--- a/base/resources/binaries/bazel-buildifier
+++ b/base/resources/binaries/bazel-buildifier
Binary files differ
diff --git a/base/src/META-INF/blaze-base.xml b/base/src/META-INF/blaze-base.xml
index 913bdb5..f472193 100644
--- a/base/src/META-INF/blaze-base.xml
+++ b/base/src/META-INF/blaze-base.xml
@@ -56,12 +56,20 @@
       class="com.google.idea.blaze.base.settings.ui.OpenLocalProjectViewAction"
       text="Open Local Project View File">
     </action>
-    <action class="com.google.idea.blaze.base.buildmap.OpenCorrespondingBuildFile"
-      id="Blaze.OpenCorrespondingBuildFile"
+    <action id="Blaze.AddDirectoryToProjectView"
+      class="com.google.idea.blaze.base.settings.ui.AddDirectoryToProjectAction"
+      text="Add Directory To Project...">
+    </action>
+    <action id="Blaze.OpenCorrespondingBuildFile"
+      class="com.google.idea.blaze.base.buildmap.OpenCorrespondingBuildFile"
       text="Open Corresponding BUILD File">
     </action>
-    <action class="com.google.idea.blaze.base.sync.actions.PartialSyncAction"
-      id="Blaze.PartialSync"
+    <action id="Blaze.CopyBlazeTargetPathAction"
+      class="com.google.idea.blaze.base.actions.CopyBlazeTargetPathAction"
+      text="Copy BUILD target string">
+    </action>
+    <action id="Blaze.PartialSync"
+      class="com.google.idea.blaze.base.sync.actions.PartialSyncAction"
       text="Partially Sync File"
       icon="BlazeIcons.Blaze">
     </action>
@@ -77,12 +85,15 @@
       class="com.google.idea.blaze.base.ide.NewBlazeRuleAction"
       text="New Rule"
       popup="true"/>
+    <action id="Blaze.OpenWorkspaceFile"
+      class="com.google.idea.blaze.base.ide.OpenBlazeWorkspaceFileAction"
+      text="Open Workspace File..."
+      icon="BlazeIcons.Blaze">
+      <add-to-group group-id="FileOpenGroup" relative-to-action="OpenFile" anchor="after"/>
+    </action>
 
     <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="Blaze.EditLocalProjectView"/>
-      <reference id="Blaze.EditProjectView"/>
-      <separator/>
       <group id ="Blaze.SyncMenuGroup" text="Sync" popup="true">
         <reference id="Blaze.IncrementalSyncProject"/>
         <reference id="Blaze.FullSyncProject"/>
@@ -95,6 +106,11 @@
         <reference id="MakeBlazeProject"/>
         <reference id="MakeBlazeModule"/>
       </group>
+      <group id="Blaze.Project" text="Project" popup="true">
+        <reference id="Blaze.EditLocalProjectView"/>
+        <reference id="Blaze.EditProjectView"/>
+        <reference id="Blaze.AddDirectoryToProjectView"/>
+      </group>
       <!--Add popup groups anchored after this bookmark-->
       <group id="Blaze.MenuGroupsBookmark"/>
       <separator/>
@@ -123,6 +139,7 @@
       <separator/>
       <reference ref="Blaze.PartialSync"/>
       <reference ref="Blaze.OpenCorrespondingBuildFile"/>
+      <reference ref="Blaze.CopyBlazeTargetPathAction"/>
     </group>
   </actions>
 
@@ -162,8 +179,8 @@
                     serviceImplementation="com.google.idea.blaze.base.buildmodifier.FileSystemModifierImpl"/>
     <applicationService serviceInterface="com.google.idea.blaze.base.run.targetfinder.TargetFinder"
                         serviceImplementation="com.google.idea.blaze.base.run.targetfinder.TargetFinderImpl"/>
-    <applicationService serviceInterface="com.google.idea.blaze.base.command.info.BlazeInfo"
-                        serviceImplementation="com.google.idea.blaze.base.command.info.BlazeInfoImpl"/>
+    <applicationService serviceInterface="com.google.idea.blaze.base.command.info.BlazeInfoRunner"
+                        serviceImplementation="com.google.idea.blaze.base.command.info.BlazeInfoRunnerImpl"/>
 
     <treeStructureProvider implementation="com.google.idea.blaze.base.treeview.BlazeTreeStructureProvider" id="blaze"/>
 
@@ -183,12 +200,14 @@
     <projectService serviceInterface="com.google.idea.blaze.base.targetmaps.TransitiveDependencyMap"
                     serviceImplementation="com.google.idea.blaze.base.targetmaps.TransitiveDependencyMap"/>
     <projectService serviceImplementation="com.google.idea.blaze.base.settings.BlazeImportSettingsManager"/>
-    <projectService serviceImplementation="com.google.idea.blaze.base.settings.BlazeImportSettingsManagerLegacy"/>
     <applicationService serviceImplementation="com.google.idea.blaze.base.settings.BlazeUserSettings"/>
     <applicationService serviceInterface="com.google.idea.blaze.base.lang.buildfile.language.semantics.BuildLanguageSpecProvider"
                         serviceImplementation="com.google.idea.blaze.base.lang.buildfile.language.semantics.BuildLanguageSpecProviderImpl"/>
     <applicationService serviceInterface="com.google.idea.blaze.base.sync.projectstructure.ModuleEditorProvider"
                         serviceImplementation="com.google.idea.blaze.base.sync.projectstructure.ModuleEditorProviderImpl"/>
+    <projectService serviceInterface="com.google.idea.blaze.base.sync.projectstructure.ModuleFinder"
+                    serviceImplementation="com.google.idea.blaze.base.sync.projectstructure.ModuleFinderImpl"/>
+    <applicationService serviceImplementation="com.google.idea.blaze.base.sync.projectview.RelatedWorkspacePathFinder"/>
     <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"/>
@@ -225,6 +244,9 @@
     <completion.contributor language="projectview" implementationClass="com.google.idea.blaze.base.lang.projectview.completion.WorkspaceTypeCompletionContributor"/>
     <completion.contributor language="projectview" implementationClass="com.google.idea.blaze.base.lang.projectview.completion.AdditionalLanguagesCompletionContributor"/>
     <enterHandlerDelegate implementation="com.google.idea.blaze.base.lang.projectview.formatting.ProjectViewEnterHandler"/>
+    <filetype.stubBuilder filetype="projectview" implementationClass="com.google.idea.blaze.base.lang.projectview.stubs.ProjectViewFileStubBuilder"/>
+    <lang.documentationProvider language="projectview" implementationClass="com.google.idea.blaze.base.lang.projectview.documentation.ProjectViewDocumentationProvider"/>
+    <langCodeStyleSettingsProvider implementation="com.google.idea.blaze.base.lang.projectview.formatting.ProjectViewCodeStyleSettingsProvider"/>
   </extensions>
 
   <extensions defaultExtensionNs="com.intellij">
@@ -233,10 +255,11 @@
     <!--<annotator language="BUILD" implementationClass="com.google.idea.blaze.base.lang.buildfile.validation.LoadErrorAnnotator"/>-->
     <annotator language="BUILD" implementationClass="com.google.idea.blaze.base.lang.buildfile.validation.GlobErrorAnnotator"/>
     <annotator language="BUILD" implementationClass="com.google.idea.blaze.base.lang.buildfile.validation.BuiltInRuleAnnotator"/>
+    <annotator language="BUILD" implementationClass="com.google.idea.blaze.base.lang.buildfile.validation.LoadStatementAnnotator"/>
     <colorSettingsPage implementation="com.google.idea.blaze.base.lang.buildfile.highlighting.BuildColorsPage"/>
     <projectService serviceImplementation="com.google.idea.blaze.base.lang.buildfile.psi.util.BuildElementGenerator"/>
     <projectService serviceImplementation="com.google.idea.blaze.base.lang.buildfile.references.BuildReferenceManager"/>
-    <referencesSearch implementation="com.google.idea.blaze.base.lang.buildfile.search.BuildLabelReferenceSearcher"/>
+    <referencesSearch implementation="com.google.idea.blaze.base.lang.buildfile.search.BuildReferenceSearcher"/>
     <referencesSearch implementation="com.google.idea.blaze.base.lang.buildfile.search.GlobReferenceSearcher"/>
     <readWriteAccessDetector implementation="com.google.idea.blaze.base.lang.buildfile.findusages.BuildReadWriteAccessDetector"/>
     <elementDescriptionProvider implementation="com.google.idea.blaze.base.lang.buildfile.findusages.BuildElementDescriptionProvider"/>
@@ -257,6 +280,7 @@
     <filetype.stubBuilder filetype="BUILD" implementationClass="com.google.idea.blaze.base.lang.buildfile.stubs.BuildFileStubBuilder"/>
     <editorNotificationProvider implementation="com.google.idea.blaze.base.lang.AdditionalLanguagesHelper"/>
     <usageTypeProvider implementation="com.google.idea.blaze.base.lang.buildfile.findusages.BuildUsageTypeProvider"/>
+    <renameInputValidator implementation="com.google.idea.blaze.base.lang.buildfile.refactor.TargetRenameValidator"/>
   </extensions>
 
   <extensions defaultExtensionNs="com.intellij.lang">
@@ -289,6 +313,9 @@
       <interface-class>com.google.idea.common.experiments.ExperimentService</interface-class>
       <implementation-class>com.google.idea.blaze.base.experiments.BlazeExperimentService</implementation-class>
     </component>
+    <component>
+      <implementation-class>com.google.idea.blaze.base.prefetch.PrefetchProjectInitializer</implementation-class>
+    </component>
   </application-components>
 
   <project-components>
@@ -323,9 +350,11 @@
     <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"/>
     <extensionPoint qualifiedName="com.google.idea.blaze.FileStringParser" interface="com.google.idea.blaze.base.run.filter.FileResolver"/>
-    <extensionPoint qualifiedName="com.google.idea.blaze.BlazeTestXmlFinderStrategy" interface="com.google.idea.blaze.base.run.testlogs.BlazeTestXmlFinderStrategy"/>
+    <extensionPoint qualifiedName="com.google.idea.blaze.BlazeTestXmlFinderStrategy" interface="com.google.idea.blaze.base.run.testlogs.BlazeTestResultFinderStrategy"/>
     <extensionPoint qualifiedName="com.google.idea.blaze.BlazeTestEventsHandler" interface="com.google.idea.blaze.base.run.smrunner.BlazeTestEventsHandler"/>
     <extensionPoint qualifiedName="com.google.idea.blaze.AttributeSpecificStringLiteralReferenceProvider" interface="com.google.idea.blaze.base.lang.buildfile.references.AttributeSpecificStringLiteralReferenceProvider"/>
+    <extensionPoint qualifiedName="com.google.idea.blaze.EventLogger" interface="com.google.idea.blaze.base.logging.EventLogger"/>
+    <extensionPoint qualifiedName="com.google.idea.blaze.ProjectViewDefaultValueProvider" interface="com.google.idea.blaze.base.projectview.section.ProjectViewDefaultValueProvider"/>
   </extensionPoints>
 
   <extensions defaultExtensionNs="com.google.idea.blaze">
@@ -339,13 +368,17 @@
     <BuildSystemProvider implementation="com.google.idea.blaze.base.bazel.BazelBuildSystemProvider" order="last"/>
     <BuildifierBinaryProvider implementation="com.google.idea.blaze.base.buildmodifier.BazelBuildifierBinaryProvider"/>
     <BlazeCommandRunConfigurationHandlerProvider implementation="com.google.idea.blaze.base.run.confighandler.BlazeCommandGenericRunConfigurationHandlerProvider" order="last"/>
-    <TestTargetHeuristic implementation="com.google.idea.blaze.base.run.TargetNameHeuristic" order="first"/>
+    <TestTargetHeuristic implementation="com.google.idea.blaze.base.run.TargetNameHeuristic" order="first" id="TargetNameHeuristic"/>
+    <TestTargetHeuristic implementation="com.google.idea.blaze.base.run.TestTargetSourcesHeuristic"/>
     <TestTargetHeuristic implementation="com.google.idea.blaze.base.run.TestSizeHeuristic" order="last" id="TestSizeHeuristic"/>
     <RunConfigurationFactory implementation="com.google.idea.blaze.base.run.BlazeBuildTargetRunConfigurationFactory" order="last"/>
     <AspectStrategyProvider implementation="com.google.idea.blaze.base.sync.aspects.strategy.AspectStrategyProviderBazel" order="last"/>
     <FileStringParser implementation="com.google.idea.blaze.base.run.filter.StandardFileResolver" order="last"/>
-    <BlazeTestXmlFinderStrategy implementation="com.google.idea.blaze.base.run.testlogs.TargetPathTestXmlFinderStrategy"/>
+    <BlazeTestXmlFinderStrategy implementation="com.google.idea.blaze.base.run.testlogs.TargetPathTestResultFinderStrategy"/>
     <BlazeTestEventsHandler implementation="com.google.idea.blaze.base.run.smrunner.BlazeCompositeTestEventsHandler" order="last"/>
+    <ProjectViewDefaultValueProvider implementation="com.google.idea.blaze.base.projectview.section.sections.DirectorySection$DirectoriesProjectViewDefaultValueProvider"/>
+    <ProjectViewDefaultValueProvider implementation="com.google.idea.blaze.base.projectview.section.sections.TargetSection$TargetsProjectViewDefaultValueProvider"/>
+    <ProjectViewDefaultValueProvider implementation="com.google.idea.blaze.base.projectview.section.sections.AdditionalLanguagesSection$AdditionalLanguagesDefaultValueProvider"/>
   </extensions>
 
 </idea-plugin>
diff --git a/base/src/com/google/idea/blaze/base/actions/BlazeBuildService.java b/base/src/com/google/idea/blaze/base/actions/BlazeBuildService.java
index 6b9e095..36a23e9 100644
--- a/base/src/com/google/idea/blaze/base/actions/BlazeBuildService.java
+++ b/base/src/com/google/idea/blaze/base/actions/BlazeBuildService.java
@@ -37,7 +37,10 @@
 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.BuildResult;
 import com.google.idea.blaze.base.sync.data.BlazeProjectDataManager;
+import com.google.idea.blaze.base.sync.sharding.BlazeBuildTargetSharder;
+import com.google.idea.blaze.base.sync.sharding.BlazeBuildTargetSharder.ShardedTargetsResult;
 import com.google.idea.blaze.base.util.SaveUtil;
 import com.intellij.openapi.components.ServiceManager;
 import com.intellij.openapi.project.Project;
@@ -113,20 +116,31 @@
                 .push(notificationScope);
 
             WorkspaceRoot workspaceRoot = WorkspaceRoot.fromProject(project);
-            BlazeIdeInterface blazeIdeInterface = BlazeIdeInterface.getInstance();
 
             SaveUtil.saveAllFiles();
-            BlazeIdeInterface.BuildResult buildResult =
-                blazeIdeInterface.compileIdeArtifacts(
+            ShardedTargetsResult shardedTargets =
+                BlazeBuildTargetSharder.expandAndShardTargets(
                     project,
                     context,
                     workspaceRoot,
                     projectViewSet,
-                    blazeProjectData.blazeVersionData,
+                    blazeProjectData.workspacePathResolver,
                     targets);
+            if (shardedTargets.buildResult.status == BuildResult.Status.FATAL_ERROR) {
+              return;
+            }
+            BuildResult buildResult =
+                BlazeIdeInterface.getInstance()
+                    .compileIdeArtifacts(
+                        project,
+                        context,
+                        workspaceRoot,
+                        projectViewSet,
+                        blazeProjectData.blazeVersionData,
+                        shardedTargets.shardedTargets);
             FileCaches.refresh(project);
 
-            if (buildResult != BlazeIdeInterface.BuildResult.SUCCESS) {
+            if (buildResult.status != BuildResult.Status.SUCCESS) {
               context.setHasError();
             }
           }
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 a124759..b049dee 100644
--- a/base/src/com/google/idea/blaze/base/actions/BlazeMakeProjectAction.java
+++ b/base/src/com/google/idea/blaze/base/actions/BlazeMakeProjectAction.java
@@ -15,6 +15,8 @@
  */
 package com.google.idea.blaze.base.actions;
 
+import com.google.common.collect.ImmutableMap;
+import com.google.idea.blaze.base.logging.EventLogger;
 import com.intellij.openapi.actionSystem.AnActionEvent;
 import com.intellij.openapi.project.Project;
 
@@ -22,6 +24,7 @@
 
   @Override
   protected void actionPerformedInBlazeProject(Project project, AnActionEvent e) {
+    EventLogger.getInstance().log(getClass(), "make", ImmutableMap.of());
     BlazeBuildService.getInstance().buildProject(project);
   }
 }
diff --git a/base/src/com/google/idea/blaze/base/actions/CopyBlazeTargetPathAction.java b/base/src/com/google/idea/blaze/base/actions/CopyBlazeTargetPathAction.java
new file mode 100644
index 0000000..ffb49e9
--- /dev/null
+++ b/base/src/com/google/idea/blaze/base/actions/CopyBlazeTargetPathAction.java
@@ -0,0 +1,54 @@
+/*
+ * Copyright 2017 The Bazel Authors. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.idea.blaze.base.actions;
+
+import com.google.idea.blaze.base.lang.buildfile.psi.FuncallExpression;
+import com.google.idea.blaze.base.model.primitives.Label;
+import com.google.idea.common.actionhelper.ActionPresentationHelper;
+import com.intellij.openapi.actionSystem.AnActionEvent;
+import com.intellij.openapi.actionSystem.CommonDataKeys;
+import com.intellij.openapi.ide.CopyPasteManager;
+import com.intellij.openapi.project.Project;
+import com.intellij.psi.PsiElement;
+import java.awt.datatransfer.StringSelection;
+import javax.annotation.Nullable;
+
+/** Copies a blaze target path into the clipboard */
+public class CopyBlazeTargetPathAction extends BlazeProjectAction {
+
+  @Override
+  protected void actionPerformedInBlazeProject(Project project, AnActionEvent e) {
+    Label label = getSelectedTarget(e);
+    if (label != null) {
+      CopyPasteManager.getInstance().setContents(new StringSelection(label.toString()));
+    }
+  }
+
+  @Override
+  protected void updateForBlazeProject(Project project, AnActionEvent e) {
+    Label label = getSelectedTarget(e);
+    ActionPresentationHelper.of(e).hideIf(label == null).commit();
+  }
+
+  @Nullable
+  private static Label getSelectedTarget(AnActionEvent e) {
+    PsiElement psiElement = e.getData(CommonDataKeys.PSI_ELEMENT);
+    if (!(psiElement instanceof FuncallExpression)) {
+      return null;
+    }
+    return ((FuncallExpression) psiElement).resolveBuildLabel();
+  }
+}
diff --git a/base/src/com/google/idea/blaze/base/async/executor/BlazeExecutor.java b/base/src/com/google/idea/blaze/base/async/executor/BlazeExecutor.java
index f9eed17..8df0ed0 100644
--- a/base/src/com/google/idea/blaze/base/async/executor/BlazeExecutor.java
+++ b/base/src/com/google/idea/blaze/base/async/executor/BlazeExecutor.java
@@ -28,8 +28,8 @@
 import com.intellij.openapi.util.Computable;
 import com.intellij.util.ui.UIUtil;
 import java.util.concurrent.Callable;
+import javax.annotation.Nullable;
 import org.jetbrains.annotations.NotNull;
-import org.jetbrains.annotations.Nullable;
 
 /** Shared thread pool for blaze tasks. */
 public abstract class BlazeExecutor {
@@ -59,7 +59,7 @@
       @NotNull final String title,
       @NotNull final Progressive progressive) {
     return submitTask(
-        project, title, true /* cancelable */, Modality.ALWAYS_BACKGROUND, progressive);
+        project, title, /* cancelable */ true, Modality.ALWAYS_BACKGROUND, progressive);
   }
 
   public static ListenableFuture<Void> submitTask(
diff --git a/base/src/com/google/idea/blaze/base/async/process/LineProcessingOutputStream.java b/base/src/com/google/idea/blaze/base/async/process/LineProcessingOutputStream.java
index 8aa44a8..5357fdf 100644
--- a/base/src/com/google/idea/blaze/base/async/process/LineProcessingOutputStream.java
+++ b/base/src/com/google/idea/blaze/base/async/process/LineProcessingOutputStream.java
@@ -15,11 +15,11 @@
  */
 package com.google.idea.blaze.base.async.process;
 
-import com.google.common.collect.Lists;
+import static java.nio.charset.StandardCharsets.UTF_8;
+
+import com.google.common.collect.ImmutableList;
 import java.io.IOException;
 import java.io.OutputStream;
-import java.util.List;
-import org.jetbrains.annotations.NotNull;
 
 /** An base output stream which marshals output into newline-delimited segments for processing. */
 public final class LineProcessingOutputStream extends OutputStream {
@@ -31,25 +31,29 @@
      *
      * @return Whether line processing should continue
      */
-    boolean processLine(@NotNull String line);
+    boolean processLine(String line);
   }
 
-  @NotNull private final StringBuffer stringBuffer = new StringBuffer();
+  private final StringBuffer stringBuffer = new StringBuffer();
   private volatile boolean closed;
-  @NotNull private final List<LineProcessor> lineProcessors;
+  private final ImmutableList<LineProcessor> lineProcessors;
 
-  LineProcessingOutputStream(@NotNull LineProcessor... lineProcessors) {
-    this.lineProcessors = Lists.newArrayList(lineProcessors);
+  LineProcessingOutputStream(ImmutableList<LineProcessor> lineProcessors) {
+    this.lineProcessors = lineProcessors;
   }
 
-  public static LineProcessingOutputStream of(@NotNull LineProcessor... lineProcessors) {
+  public static LineProcessingOutputStream of(LineProcessor... lineProcessors) {
+    return new LineProcessingOutputStream(ImmutableList.copyOf(lineProcessors));
+  }
+
+  public static LineProcessingOutputStream of(ImmutableList<LineProcessor> lineProcessors) {
     return new LineProcessingOutputStream(lineProcessors);
   }
 
   @Override
   public synchronized void write(byte[] b, int off, int len) {
     if (!closed) {
-      String text = new String(b, off, len);
+      String text = new String(b, off, len, UTF_8);
       stringBuffer.append(text);
 
       while (true) {
diff --git a/base/src/com/google/idea/blaze/base/bazel/BazelBuildSystemProvider.java b/base/src/com/google/idea/blaze/base/bazel/BazelBuildSystemProvider.java
index b3d4598..85296be 100644
--- a/base/src/com/google/idea/blaze/base/bazel/BazelBuildSystemProvider.java
+++ b/base/src/com/google/idea/blaze/base/bazel/BazelBuildSystemProvider.java
@@ -16,13 +16,15 @@
 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.command.info.BlazeInfo;
 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.google.idea.blaze.base.settings.BlazeUserSettings;
 import com.intellij.openapi.fileTypes.ExactFileNameMatcher;
+import com.intellij.openapi.fileTypes.ExtensionFileNameMatcher;
 import com.intellij.openapi.fileTypes.FileNameMatcher;
 import java.io.File;
 import javax.annotation.Nullable;
@@ -35,6 +37,13 @@
     return BuildSystem.Bazel;
   }
 
+  @Nullable
+  @Override
+  public String getBinaryPath() {
+    BlazeUserSettings settings = BlazeUserSettings.getInstance();
+    return settings.getBazelBinaryPath();
+  }
+
   @Override
   public WorkspaceRootProvider getWorkspaceRootProvider() {
     return BazelWorkspaceRootProvider.INSTANCE;
@@ -47,17 +56,20 @@
         "bazel-bin", "bazel-genfiles", "bazel-out", "bazel-testlogs", "bazel-" + rootDir);
   }
 
-  @Nullable
   @Override
   public String getRuleDocumentationUrl(RuleDefinition rule) {
     // TODO: URL pointing to specific BUILD rule.
     return "http://www.bazel.build/docs/be/overview.html";
   }
 
-  // TODO: Update the methods below when https://github.com/bazelbuild/bazel/issues/552 lands.
+  @Override
+  public String getProjectViewDocumentationUrl() {
+    return "https://ij.bazel.build/docs/project-views.html";
+  }
+
   @Override
   public boolean isBuildFile(String fileName) {
-    return fileName.equals("BUILD");
+    return fileName.equals("BUILD") || fileName.equals("BUILD.bazel");
   }
 
   @Nullable
@@ -65,22 +77,28 @@
   public File findBuildFileInDirectory(File directory) {
     FileAttributeProvider provider = FileAttributeProvider.getInstance();
     File child = new File(directory, "BUILD");
-    if (!provider.exists(child)) {
-      return null;
+    if (provider.exists(child)) {
+      return child;
     }
-    return child;
+    child = new File(directory, "BUILD.bazel");
+    if (provider.exists(child)) {
+      return child;
+    }
+    return null;
   }
 
   @Override
-  public FileNameMatcher buildFileMatcher() {
-    return new ExactFileNameMatcher("BUILD");
+  public ImmutableList<FileNameMatcher> buildLanguageFileTypeMatchers() {
+    return ImmutableList.of(
+        new ExactFileNameMatcher("BUILD"), new ExactFileNameMatcher("BUILD.bazel"),
+        new ExtensionFileNameMatcher("bzl"), new ExactFileNameMatcher("WORKSPACE"));
   }
 
   @Override
   public void populateBlazeVersionData(
       BuildSystem buildSystem,
       WorkspaceRoot workspaceRoot,
-      ImmutableMap<String, String> blazeInfo,
+      BlazeInfo blazeInfo,
       BlazeVersionData.Builder builder) {
     if (buildSystem != BuildSystem.Bazel) {
       return;
diff --git a/base/src/com/google/idea/blaze/base/bazel/BazelVersion.java b/base/src/com/google/idea/blaze/base/bazel/BazelVersion.java
index 44582cb..19be022 100644
--- a/base/src/com/google/idea/blaze/base/bazel/BazelVersion.java
+++ b/base/src/com/google/idea/blaze/base/bazel/BazelVersion.java
@@ -21,7 +21,6 @@
 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;
@@ -77,7 +76,7 @@
     return new BazelVersion(major, minor, bugfix);
   }
 
-  public static BazelVersion parseVersion(Map<String, String> blazeInfo) {
+  public static BazelVersion parseVersion(BlazeInfo blazeInfo) {
     return parseVersion(blazeInfo.get(BlazeInfo.RELEASE));
   }
 
diff --git a/base/src/com/google/idea/blaze/base/bazel/BuildSystemProvider.java b/base/src/com/google/idea/blaze/base/bazel/BuildSystemProvider.java
index 61c7257..22392d4 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,7 @@
 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.command.info.BlazeInfo;
 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;
@@ -71,6 +71,16 @@
    */
   BuildSystem buildSystem();
 
+  /** @return The location of the blaze/bazel binary. */
+  @Nullable
+  String getBinaryPath();
+
+  /** @return The location of the blaze/bazel binary to use for syncing. */
+  @Nullable
+  default String getSyncBinaryPath() {
+    return getBinaryPath();
+  }
+
   WorkspaceRootProvider getWorkspaceRootProvider();
 
   /** Directories containing artifacts produced during the build process. */
@@ -80,6 +90,10 @@
   @Nullable
   String getRuleDocumentationUrl(RuleDefinition rule);
 
+  /** The URL providing documentation for project view files, if one can be found. */
+  @Nullable
+  String getProjectViewDocumentationUrl();
+
   /** Check if the given filename is a valid BUILD file name. */
   boolean isBuildFile(String fileName);
 
@@ -101,12 +115,13 @@
     return buildFile != null ? directory.getFileSystem().findFileByPath(buildFile.getPath()) : null;
   }
 
-  FileNameMatcher buildFileMatcher();
+  /** Returns the list of file types recognized as build system files. */
+  ImmutableList<FileNameMatcher> buildLanguageFileTypeMatchers();
 
   /** Populates the passed builder with version data. */
   void populateBlazeVersionData(
       BuildSystem buildSystem,
       WorkspaceRoot workspaceRoot,
-      ImmutableMap<String, String> blazeInfo,
+      BlazeInfo blazeInfo,
       BlazeVersionData.Builder builder);
 }
diff --git a/base/src/com/google/idea/blaze/base/buildmodifier/BuildifierDelegatingCodeStyleManager.java b/base/src/com/google/idea/blaze/base/buildmodifier/BuildifierDelegatingCodeStyleManager.java
index cf4574a..ca0827d 100644
--- a/base/src/com/google/idea/blaze/base/buildmodifier/BuildifierDelegatingCodeStyleManager.java
+++ b/base/src/com/google/idea/blaze/base/buildmodifier/BuildifierDelegatingCodeStyleManager.java
@@ -78,7 +78,7 @@
   private static boolean overrideFormatterForFile(PsiFile file) {
     // don't format skylark extensions
     return file instanceof BuildFile
-        && ((BuildFile) file).getBlazeFileType() == BlazeFileType.BuildPackage;
+        && ((BuildFile) file).getBlazeFileType() != BlazeFileType.SkylarkExtension;
   }
 
   private void formatInternal(PsiFile file, Collection<TextRange> ranges) {
diff --git a/base/src/com/google/idea/blaze/base/buildmodifier/FileSystemModifier.java b/base/src/com/google/idea/blaze/base/buildmodifier/FileSystemModifier.java
index f3586a2..96719c1 100644
--- a/base/src/com/google/idea/blaze/base/buildmodifier/FileSystemModifier.java
+++ b/base/src/com/google/idea/blaze/base/buildmodifier/FileSystemModifier.java
@@ -19,8 +19,8 @@
 import com.intellij.openapi.components.ServiceManager;
 import com.intellij.openapi.project.Project;
 import java.io.File;
+import javax.annotation.Nullable;
 import org.jetbrains.annotations.NotNull;
-import org.jetbrains.annotations.Nullable;
 
 /** Modifies the file system. Interface so we can mock it in tests */
 public abstract class FileSystemModifier {
diff --git a/base/src/com/google/idea/blaze/base/buildmodifier/FileSystemModifierImpl.java b/base/src/com/google/idea/blaze/base/buildmodifier/FileSystemModifierImpl.java
index 0d398f9..7e873ee 100644
--- a/base/src/com/google/idea/blaze/base/buildmodifier/FileSystemModifierImpl.java
+++ b/base/src/com/google/idea/blaze/base/buildmodifier/FileSystemModifierImpl.java
@@ -20,8 +20,8 @@
 import com.intellij.openapi.project.Project;
 import java.io.File;
 import java.io.IOException;
+import javax.annotation.Nullable;
 import org.jetbrains.annotations.NotNull;
-import org.jetbrains.annotations.Nullable;
 
 class FileSystemModifierImpl extends FileSystemModifier {
 
diff --git a/base/src/com/google/idea/blaze/base/command/BlazeCommand.java b/base/src/com/google/idea/blaze/base/command/BlazeCommand.java
index a337472..d43aeaa 100644
--- a/base/src/com/google/idea/blaze/base/command/BlazeCommand.java
+++ b/base/src/com/google/idea/blaze/base/command/BlazeCommand.java
@@ -18,30 +18,21 @@
 import com.google.common.base.Joiner;
 import com.google.common.collect.ImmutableList;
 import com.google.idea.blaze.base.model.primitives.TargetExpression;
-import com.google.idea.blaze.base.settings.Blaze.BuildSystem;
-import com.google.idea.blaze.base.settings.BlazeUserSettings;
 import java.util.Arrays;
 import java.util.List;
-import javax.annotation.Nullable;
 import javax.annotation.concurrent.Immutable;
 
 /** A command to issue to Blaze/Bazel on the command line. */
 @Immutable
 public final class BlazeCommand {
 
-  private final BuildSystem buildSystem;
+  private final String binaryPath;
   private final BlazeCommandName name;
-  @Nullable private final String blazeBinary;
   private final ImmutableList<String> arguments;
 
-  private BlazeCommand(
-      BuildSystem buildSystem,
-      BlazeCommandName name,
-      @Nullable String blazeBinary,
-      ImmutableList<String> arguments) {
-    this.buildSystem = buildSystem;
+  private BlazeCommand(String binaryPath, BlazeCommandName name, ImmutableList<String> arguments) {
+    this.binaryPath = binaryPath;
     this.name = name;
-    this.blazeBinary = blazeBinary;
     this.arguments = arguments;
   }
 
@@ -50,27 +41,11 @@
   }
 
   public ImmutableList<String> toList() {
-    String blazeBinary = this.blazeBinary;
-    if (blazeBinary == null) {
-      blazeBinary = getBinaryPath(buildSystem);
-    }
-
-    ImmutableList.Builder<String> commandLine = ImmutableList.builder();
-    commandLine.add(blazeBinary, name.toString());
-    commandLine.addAll(arguments);
-    return commandLine.build();
-  }
-
-  private static String getBinaryPath(BuildSystem buildSystem) {
-    BlazeUserSettings settings = BlazeUserSettings.getInstance();
-    switch (buildSystem) {
-      case Blaze:
-        return settings.getBlazeBinaryPath();
-      case Bazel:
-        return settings.getBazelBinaryPath();
-      default:
-        throw new RuntimeException("Unrecognized build system type: " + buildSystem);
-    }
+    return ImmutableList.<String>builder()
+        .add(binaryPath)
+        .add(name.toString())
+        .addAll(arguments)
+        .build();
   }
 
   @Override
@@ -78,21 +53,20 @@
     return Joiner.on(' ').join(toList());
   }
 
-  public static Builder builder(BuildSystem buildSystem, BlazeCommandName name) {
-    return new Builder(buildSystem, name);
+  public static Builder builder(String binaryPath, BlazeCommandName name) {
+    return new Builder(binaryPath, name);
   }
 
   /** Builder for a blaze command */
   public static class Builder {
-    private final BuildSystem buildSystem;
+    private final String binaryPath;
     private final BlazeCommandName name;
-    @Nullable private String blazeBinary;
     private final ImmutableList.Builder<TargetExpression> targets = ImmutableList.builder();
     private final ImmutableList.Builder<String> blazeFlags = ImmutableList.builder();
     private final ImmutableList.Builder<String> exeFlags = ImmutableList.builder();
 
-    public Builder(BuildSystem buildSystem, BlazeCommandName name) {
-      this.buildSystem = buildSystem;
+    public Builder(String binaryPath, BlazeCommandName name) {
+      this.binaryPath = binaryPath;
       this.name = name;
       // Tell forge what tool we used to call blaze so we can track usage.
       addBlazeFlags(BlazeFlags.getToolTagFlag());
@@ -109,12 +83,7 @@
       }
 
       arguments.addAll(exeFlags.build());
-      return new BlazeCommand(buildSystem, name, blazeBinary, arguments.build());
-    }
-
-    public Builder setBlazeBinary(@Nullable String blazeBinary) {
-      this.blazeBinary = blazeBinary;
-      return this;
+      return new BlazeCommand(binaryPath, name, arguments.build());
     }
 
     public Builder addTargets(TargetExpression... targets) {
diff --git a/base/src/com/google/idea/blaze/base/command/BlazeFlags.java b/base/src/com/google/idea/blaze/base/command/BlazeFlags.java
index 1f24f62..a44b056 100644
--- a/base/src/com/google/idea/blaze/base/command/BlazeFlags.java
+++ b/base/src/com/google/idea/blaze/base/command/BlazeFlags.java
@@ -21,6 +21,8 @@
 import com.google.idea.blaze.base.projectview.section.sections.BuildFlagsSection;
 import com.google.idea.blaze.base.settings.Blaze;
 import com.google.idea.blaze.base.settings.Blaze.BuildSystem;
+import com.google.idea.common.experiments.BoolExperiment;
+import com.intellij.execution.configurations.ParametersList;
 import com.intellij.openapi.project.Project;
 import com.intellij.util.PlatformUtils;
 import java.util.List;
@@ -28,6 +30,9 @@
 /** The collection of all the Bazel flag strings we use. */
 public final class BlazeFlags {
 
+  private static final BoolExperiment macroExpandBuildFlags =
+      new BoolExperiment("macro.expand.blaze.flags", true);
+
   // Build the maximum number of possible dependencies of the project and to show all the build
   // errors in single go.
   public static final String KEEP_GOING = "--keep_going";
@@ -39,6 +44,7 @@
   public static final String JAVA_BINARY_DEBUG = "--wrapper_script_flag=--debug";
   // Runs tests locally, in sequence (rather than parallel), and streams their results to stdout.
   public static final String TEST_OUTPUT_STREAMED = "--test_output=streamed";
+  public static final String DISABLE_TEST_SHARDING = "--test_sharding_strategy=disabled";
   // Filters the unit tests that are run (used with regexp for Java/Robolectric tests).
   public static final String TEST_FILTER = "--test_filter";
   // When used with mobile-install, deploys the an app incrementally.
@@ -48,9 +54,6 @@
   public static final String SPLIT_APKS = "--split_apks";
   // Re-run the test even if the results are cached.
   public static final String NO_CACHE_TEST_RESULTS = "--nocache_test_results";
-  // Avoids node GC between ide_build_info and blaze build
-  public static final String VERSION_WINDOW_FOR_DIRTY_NODE_GC =
-      "--version_window_for_dirty_node_gc=-1";
 
   public static final String EXPERIMENTAL_SHOW_ARTIFACTS = "--experimental_show_artifacts";
 
@@ -60,7 +63,7 @@
     for (BuildFlagsProvider buildFlagsProvider : BuildFlagsProvider.EP_NAME.getExtensions()) {
       buildFlagsProvider.addBuildFlags(buildSystem, projectViewSet, flags);
     }
-    flags.addAll(projectViewSet.listItems(BuildFlagsSection.KEY));
+    flags.addAll(expandBuildFlags(projectViewSet.listItems(BuildFlagsSection.KEY)));
     return flags;
   }
 
@@ -90,5 +93,17 @@
     return TOOL_TAG + platformPrefix;
   }
 
+  /** Expands any macros in the passed build flags. */
+  public static List<String> expandBuildFlags(List<String> flags) {
+    if (!macroExpandBuildFlags.getValue()) {
+      return flags;
+    }
+    // This built-in IntelliJ class will do macro expansion using
+    // both your enviroment and your Settings > Behavior > Path Variables
+    ParametersList parametersList = new ParametersList();
+    parametersList.addAll(flags);
+    return parametersList.getList();
+  }
+
   private BlazeFlags() {}
 }
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 2e40662..fcdfc70 100644
--- a/base/src/com/google/idea/blaze/base/command/BuildFlagsProviderImpl.java
+++ b/base/src/com/google/idea/blaze/base/command/BuildFlagsProviderImpl.java
@@ -15,26 +15,19 @@
  */
 package com.google.idea.blaze.base.command;
 
-import static com.google.idea.blaze.base.command.BlazeFlags.VERSION_WINDOW_FOR_DIRTY_NODE_GC;
-
 import com.google.idea.blaze.base.projectview.ProjectViewSet;
 import com.google.idea.blaze.base.settings.Blaze.BuildSystem;
-import com.google.idea.common.experiments.BoolExperiment;
 import java.util.List;
 
 /** Flags added to blaze/bazel build commands. */
 public class BuildFlagsProviderImpl implements BuildFlagsProvider {
 
-  private static final BoolExperiment experimentUseVersionWindowForDirtyNodeGc =
-      new BoolExperiment("ide_build_info.use_version_window_for_dirty_node_gc", false);
-
   @Override
   public void addBuildFlags(
       BuildSystem buildSystem, ProjectViewSet projectViewSet, List<String> flags) {
-    if (experimentUseVersionWindowForDirtyNodeGc.getValue()) {
-      flags.add(VERSION_WINDOW_FOR_DIRTY_NODE_GC);
-    }
     flags.add("--curses=no");
     flags.add("--color=no");
+    flags.add("--noexperimental_ui");
+    flags.add("--noprogress_in_terminal_title");
   }
 }
diff --git a/base/src/com/google/idea/blaze/base/command/ExperimentalShowArtifactsLineProcessor.java b/base/src/com/google/idea/blaze/base/command/ExperimentalShowArtifactsLineProcessor.java
deleted file mode 100644
index 8d512c0..0000000
--- a/base/src/com/google/idea/blaze/base/command/ExperimentalShowArtifactsLineProcessor.java
+++ /dev/null
@@ -1,64 +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.command;
-
-import com.google.idea.blaze.base.async.process.LineProcessingOutputStream;
-import java.io.File;
-import java.util.List;
-import java.util.function.Predicate;
-import org.jetbrains.annotations.NotNull;
-
-/** Collects the output of --experimental_show_artifacts */
-public class ExperimentalShowArtifactsLineProcessor
-    implements LineProcessingOutputStream.LineProcessor {
-  private static final String OUTPUT_START = "Build artifacts:";
-  private static final String OUTPUT_MARKER = ">>>";
-
-  private final List<File> fileList;
-  private final Predicate<String> filter;
-  boolean insideBuildResult = false;
-
-  public ExperimentalShowArtifactsLineProcessor(List<File> fileList) {
-    this(fileList, (value) -> true);
-  }
-
-  public ExperimentalShowArtifactsLineProcessor(List<File> fileList, Predicate<String> filter) {
-    this.fileList = fileList;
-    this.filter = filter;
-  }
-
-  @Override
-  public boolean processLine(@NotNull String line) {
-    if (insideBuildResult) {
-      // Workaround for --experimental_ui: Extra newlines are inserted
-      if (line.isEmpty()) {
-        return false;
-      }
-
-      insideBuildResult = line.startsWith(OUTPUT_MARKER);
-      if (insideBuildResult) {
-        String fileName = line.substring(OUTPUT_MARKER.length());
-        if (filter.test(fileName)) {
-          fileList.add(new File(fileName));
-        }
-      }
-    }
-    if (!insideBuildResult) {
-      insideBuildResult = line.equals(OUTPUT_START);
-    }
-    return !insideBuildResult;
-  }
-}
diff --git a/base/src/com/google/idea/blaze/base/command/buildresult/BuildResultHelper.java b/base/src/com/google/idea/blaze/base/command/buildresult/BuildResultHelper.java
new file mode 100644
index 0000000..9adf5e2
--- /dev/null
+++ b/base/src/com/google/idea/blaze/base/command/buildresult/BuildResultHelper.java
@@ -0,0 +1,65 @@
+/*
+ * Copyright 2017 The Bazel Authors. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.idea.blaze.base.command.buildresult;
+
+import com.google.common.collect.ImmutableList;
+import com.google.idea.blaze.base.async.process.LineProcessingOutputStream.LineProcessor;
+import com.google.idea.common.experiments.BoolExperiment;
+import java.io.File;
+import java.io.OutputStream;
+import java.util.List;
+import java.util.function.Predicate;
+
+/** Assists in getting build artifacts from a build operation. */
+public interface BuildResultHelper {
+  // This experiment does *not* work yet and should remain off
+  BoolExperiment USE_BEP = new BoolExperiment("use.bep", false);
+
+  /**
+   * Constructs a new build result helper.
+   *
+   * @param files A filter for the output artifacts you are interested in.
+   */
+  static BuildResultHelper forFiles(Predicate<String> files) {
+    return USE_BEP.getValue()
+        ? new BuildResultHelperBep(files)
+        : new BuildResultHelperStderr(files);
+  }
+
+  /**
+   * Returns the build flags necessary for the build result helper to work.
+   *
+   * <p>The user must add these flags to their build command.
+   */
+  List<String> getBuildFlags();
+
+  /**
+   * Returns an output stream to be passed to the external task's stderr.
+   *
+   * <p>The user must pipe blaze's stderr to this output stream.
+   *
+   * @param lineProcessors Any additional line processors you want on stderr output.
+   */
+  OutputStream stderr(LineProcessor... lineProcessors);
+
+  /**
+   * Returns the build result. May only be called once the build is complete, or no artifacts will
+   * be returned.
+   *
+   * @return The build artifacts from the build operation.
+   */
+  ImmutableList<File> getBuildArtifacts();
+}
diff --git a/base/src/com/google/idea/blaze/base/command/buildresult/BuildResultHelperBep.java b/base/src/com/google/idea/blaze/base/command/buildresult/BuildResultHelperBep.java
new file mode 100644
index 0000000..8a32e85
--- /dev/null
+++ b/base/src/com/google/idea/blaze/base/command/buildresult/BuildResultHelperBep.java
@@ -0,0 +1,98 @@
+/*
+ * Copyright 2017 The Bazel Authors. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.idea.blaze.base.command.buildresult;
+
+import com.google.common.collect.ImmutableList;
+import com.google.idea.blaze.base.async.process.LineProcessingOutputStream;
+import com.google.idea.blaze.base.async.process.LineProcessingOutputStream.LineProcessor;
+import com.google.repackaged.devtools.build.lib.buildeventstream.BuildEventStreamProtos.BuildEvent;
+import com.google.repackaged.devtools.build.lib.buildeventstream.BuildEventStreamProtos.BuildEventId;
+import com.google.repackaged.devtools.build.lib.buildeventstream.BuildEventStreamProtos.BuildEventId.IdCase;
+import com.intellij.openapi.diagnostic.Logger;
+import java.io.BufferedInputStream;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.util.List;
+import java.util.UUID;
+import java.util.function.Predicate;
+
+/**
+ * Build event protocol implementation to get build results.
+ *
+ * <p>The build even protocol (BEP for short) is a proto-based protocol used by bazel to communicate
+ * build events.
+ */
+class BuildResultHelperBep implements BuildResultHelper {
+  private static final Logger logger = Logger.getInstance(BuildResultHelperBep.class);
+  private final File outputFile;
+  private final Predicate<String> fileFilter;
+  private ImmutableList<File> result;
+
+  BuildResultHelperBep(Predicate<String> fileFilter) {
+    this.fileFilter = fileFilter;
+    File tempDir = new File(System.getProperty("java.io.tmpdir"));
+    String suffix = UUID.randomUUID().toString();
+    String fileName = "intellij-bep-" + suffix;
+    this.outputFile = new File(tempDir, fileName);
+  }
+
+  @Override
+  public List<String> getBuildFlags() {
+    return ImmutableList.of("--experimental_build_event_binary_file=" + outputFile.getPath());
+  }
+
+  @Override
+  public OutputStream stderr(LineProcessor... lineProcessors) {
+    return LineProcessingOutputStream.of(ImmutableList.copyOf(lineProcessors));
+  }
+
+  @Override
+  public ImmutableList<File> getBuildArtifacts() {
+    if (result == null) {
+      result = readResult();
+    }
+    return result;
+  }
+
+  private ImmutableList<File> readResult() {
+    ImmutableList.Builder<File> result = ImmutableList.builder();
+    try (InputStream inputStream = new BufferedInputStream(new FileInputStream(outputFile))) {
+      BuildEvent buildEvent;
+      while ((buildEvent = BuildEvent.parseDelimitedFrom(inputStream)) != null) {
+        BuildEventId buildEventId = buildEvent.getId();
+        // Note: This doesn't actually work. BEP does not issue these for actions
+        // that don't execute during the build, so we can't find the files
+        // for a no-op build the way we can for --experimental_show_artifacts
+        if (buildEventId.getIdCase() == IdCase.ACTION_COMPLETED) {
+          String output = buildEventId.getActionCompleted().getPrimaryOutput();
+          if (fileFilter.test(output)) {
+            result.add(new File(output));
+          }
+        }
+      }
+    } catch (IOException e) {
+      logger.error(e);
+      return ImmutableList.of();
+    }
+    if (!outputFile.delete()) {
+      logger.warn("Could not delete BEP output file: " + outputFile);
+    }
+    return result.build();
+  }
+}
diff --git a/base/src/com/google/idea/blaze/base/command/buildresult/BuildResultHelperStderr.java b/base/src/com/google/idea/blaze/base/command/buildresult/BuildResultHelperStderr.java
new file mode 100644
index 0000000..ef34dd6
--- /dev/null
+++ b/base/src/com/google/idea/blaze/base/command/buildresult/BuildResultHelperStderr.java
@@ -0,0 +1,58 @@
+/*
+ * Copyright 2017 The Bazel Authors. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.idea.blaze.base.command.buildresult;
+
+import com.google.common.collect.ImmutableList;
+import com.google.idea.blaze.base.async.process.LineProcessingOutputStream;
+import com.google.idea.blaze.base.async.process.LineProcessingOutputStream.LineProcessor;
+import com.google.idea.blaze.base.command.BlazeFlags;
+import java.io.File;
+import java.io.OutputStream;
+import java.util.List;
+import java.util.function.Predicate;
+
+class BuildResultHelperStderr implements BuildResultHelper {
+  private final ImmutableList.Builder<File> buildArtifacts = ImmutableList.builder();
+  private final ExperimentalShowArtifactsLineProcessor experimentalShowArtifactsLineProcessor;
+  private ImmutableList<File> result;
+
+  BuildResultHelperStderr(Predicate<String> fileFilter) {
+    experimentalShowArtifactsLineProcessor =
+        new ExperimentalShowArtifactsLineProcessor(buildArtifacts, fileFilter);
+  }
+
+  @Override
+  public List<String> getBuildFlags() {
+    return ImmutableList.of(BlazeFlags.EXPERIMENTAL_SHOW_ARTIFACTS);
+  }
+
+  @Override
+  public OutputStream stderr(LineProcessor... lineProcessors) {
+    return LineProcessingOutputStream.of(
+        ImmutableList.<LineProcessor>builder()
+            .add(experimentalShowArtifactsLineProcessor)
+            .add(lineProcessors)
+            .build());
+  }
+
+  @Override
+  public ImmutableList<File> getBuildArtifacts() {
+    if (result == null) {
+      result = buildArtifacts.build();
+    }
+    return result;
+  }
+}
diff --git a/base/src/com/google/idea/blaze/base/command/buildresult/ExperimentalShowArtifactsLineProcessor.java b/base/src/com/google/idea/blaze/base/command/buildresult/ExperimentalShowArtifactsLineProcessor.java
new file mode 100644
index 0000000..609867c
--- /dev/null
+++ b/base/src/com/google/idea/blaze/base/command/buildresult/ExperimentalShowArtifactsLineProcessor.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.base.command.buildresult;
+
+import com.google.common.collect.ImmutableList;
+import com.google.idea.blaze.base.async.process.LineProcessingOutputStream;
+import java.io.File;
+import java.util.function.Predicate;
+import org.jetbrains.annotations.NotNull;
+
+/** Collects the output of --experimental_show_artifacts */
+class ExperimentalShowArtifactsLineProcessor implements LineProcessingOutputStream.LineProcessor {
+  private static final String OUTPUT_START = "Build artifacts:";
+  private static final String OUTPUT_MARKER = ">>>";
+
+  private final ImmutableList.Builder<File> fileList;
+  private final Predicate<String> filter;
+  private boolean afterBuildResult = false;
+
+  ExperimentalShowArtifactsLineProcessor(
+      ImmutableList.Builder<File> fileList, Predicate<String> filter) {
+    this.fileList = fileList;
+    this.filter = filter;
+  }
+
+  @Override
+  public boolean processLine(@NotNull String line) {
+    if (!afterBuildResult) {
+      afterBuildResult = line.equals(OUTPUT_START);
+      return !afterBuildResult;
+    }
+    if (!line.startsWith(OUTPUT_MARKER)) {
+      return true;
+    }
+    String fileName = line.substring(OUTPUT_MARKER.length());
+    if (filter.test(fileName)) {
+      fileList.add(new File(fileName));
+    }
+    return false;
+  }
+}
diff --git a/base/src/com/google/idea/blaze/base/command/info/BlazeInfo.java b/base/src/com/google/idea/blaze/base/command/info/BlazeInfo.java
index 1d329aa..bf9d694 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
@@ -15,17 +15,17 @@
  */
 package com.google.idea.blaze.base.command.info;
 
+import com.google.common.annotations.VisibleForTesting;
 import com.google.common.collect.ImmutableMap;
-import com.google.common.util.concurrent.ListenableFuture;
-import com.google.idea.blaze.base.model.primitives.WorkspaceRoot;
-import com.google.idea.blaze.base.scope.BlazeContext;
+import com.google.idea.blaze.base.model.primitives.ExecutionRootPath;
 import com.google.idea.blaze.base.settings.Blaze.BuildSystem;
-import com.intellij.openapi.components.ServiceManager;
-import java.util.List;
-import javax.annotation.Nullable;
+import com.intellij.openapi.diagnostic.Logger;
+import java.io.File;
+import java.io.Serializable;
 
-/** Runs the blaze info command. The results may be cached in the workspace. */
-public abstract class BlazeInfo {
+/** The data output by blaze info. */
+public class BlazeInfo implements Serializable {
+  public static final long serialVersionUID = 2L;
   public static final String EXECUTION_ROOT_KEY = "execution_root";
   public static final String PACKAGE_PATH_KEY = "package_path";
   public static final String BUILD_LANGUAGE = "build-language";
@@ -34,6 +34,8 @@
   public static final String COMMAND_LOG = "command_log";
   public static final String RELEASE = "release";
 
+  private static final Logger logger = Logger.getInstance(BlazeInfo.class);
+
   public static String blazeBinKey(BuildSystem buildSystem) {
     switch (buildSystem) {
       case Blaze:
@@ -67,46 +69,74 @@
     }
   }
 
-  public static BlazeInfo getInstance() {
-    return ServiceManager.getService(BlazeInfo.class);
+  private final ImmutableMap<String, String> blazeInfoMap;
+
+  private final File executionRoot;
+  private final ExecutionRootPath blazeBinExecutionRootPath;
+  private final ExecutionRootPath blazeGenfilesExecutionRootPath;
+  private final File outputBase;
+
+  public BlazeInfo(BuildSystem buildSystem, ImmutableMap<String, String> blazeInfoMap) {
+    this.blazeInfoMap = blazeInfoMap;
+    this.executionRoot = new File(getOrThrow(blazeInfoMap, EXECUTION_ROOT_KEY).trim());
+    this.blazeBinExecutionRootPath =
+        ExecutionRootPath.createAncestorRelativePath(
+            executionRoot, new File(getOrThrow(blazeInfoMap, blazeBinKey(buildSystem))));
+    this.blazeGenfilesExecutionRootPath =
+        ExecutionRootPath.createAncestorRelativePath(
+            executionRoot, new File(getOrThrow(blazeInfoMap, blazeGenfilesKey(buildSystem))));
+    this.outputBase = new File(getOrThrow(blazeInfoMap, OUTPUT_BASE_KEY).trim());
+    logger.assertTrue(blazeBinExecutionRootPath != null);
+    logger.assertTrue(blazeGenfilesExecutionRootPath != null);
   }
 
-  /**
-   * @param blazeFlags The blaze flags that will be passed to Blaze.
-   * @param key The key passed to blaze info
-   * @return The blaze info value associated with the specified key
-   */
-  public abstract ListenableFuture<String> runBlazeInfo(
-      @Nullable BlazeContext context,
-      BuildSystem buildSystem,
-      WorkspaceRoot workspaceRoot,
-      List<String> blazeFlags,
-      String key);
+  private static String getOrThrow(ImmutableMap<String, String> map, String key) {
+    String value = map.get(key);
+    if (value == null) {
+      throw new RuntimeException(String.format("Could not locate %s in info map", key));
+    }
+    return value;
+  }
 
-  /**
-   * @param blazeFlags The blaze flags that will be passed to Blaze.
-   * @param key The key passed to blaze info
-   * @return The blaze info value associated with the specified key
-   */
-  public abstract ListenableFuture<byte[]> runBlazeInfoGetBytes(
-      @Nullable BlazeContext context,
-      BuildSystem buildSystem,
-      WorkspaceRoot workspaceRoot,
-      List<String> blazeFlags,
-      String key);
+  public String get(String key) {
+    return blazeInfoMap.get(key);
+  }
 
-  /**
-   * This calls blaze info without any specific key so blaze info will return all keys and values
-   * that it has. There could be a performance cost for doing this, so the user should verify that
-   * calling this method is actually faster than calling {@link #runBlazeInfo(WorkspaceRoot, List,
-   * String)}.
-   *
-   * @param blazeFlags The blaze flags that will be passed to Blaze.
-   * @return The blaze info data fields.
-   */
-  public abstract ListenableFuture<ImmutableMap<String, String>> runBlazeInfo(
-      @Nullable BlazeContext context,
-      BuildSystem buildSystem,
-      WorkspaceRoot workspaceRoot,
-      List<String> blazeFlags);
+  public File getExecutionRoot() {
+    return executionRoot;
+  }
+
+  public ExecutionRootPath getBlazeBinExecutionRootPath() {
+    return blazeBinExecutionRootPath;
+  }
+
+  public ExecutionRootPath getBlazeGenfilesExecutionRootPath() {
+    return blazeGenfilesExecutionRootPath;
+  }
+
+  public File getGenfilesDirectory() {
+    return blazeGenfilesExecutionRootPath.getFileRootedAt(getExecutionRoot());
+  }
+
+  public File getBlazeBinDirectory() {
+    return blazeBinExecutionRootPath.getFileRootedAt(getExecutionRoot());
+  }
+
+  public File getOutputBase() {
+    return outputBase;
+  }
+
+  /** Creates a mock blaze info with the minimum information required for syncing. */
+  @VisibleForTesting
+  public static BlazeInfo createMockBlazeInfo(
+      String outputBase, String executionRoot, String blazeBin, String blazeGenFiles) {
+    BuildSystem buildSystem = BuildSystem.Bazel;
+    ImmutableMap.Builder<String, String> blazeInfoMap =
+        ImmutableMap.<String, String>builder()
+            .put(OUTPUT_BASE_KEY, outputBase)
+            .put(EXECUTION_ROOT_KEY, executionRoot)
+            .put(blazeBinKey(buildSystem), blazeBin)
+            .put(blazeGenfilesKey(buildSystem), blazeGenFiles);
+    return new BlazeInfo(buildSystem, blazeInfoMap.build());
+  }
 }
diff --git a/base/src/com/google/idea/blaze/base/command/info/BlazeInfoException.java b/base/src/com/google/idea/blaze/base/command/info/BlazeInfoException.java
index 39294c3..98aa7d3 100644
--- a/base/src/com/google/idea/blaze/base/command/info/BlazeInfoException.java
+++ b/base/src/com/google/idea/blaze/base/command/info/BlazeInfoException.java
@@ -17,33 +17,23 @@
 
 import javax.annotation.concurrent.Immutable;
 
-/** Exception occuring during blaze infoy */
+/** Exception occuring during blaze info */
 @Immutable
 public final class BlazeInfoException extends Exception {
   private final int exitCode;
   private final String stdout;
-  private final String stderr;
 
-  public BlazeInfoException(int exitCode, String stdout, String stderr) {
+  public BlazeInfoException(int exitCode, String stdout) {
     this.exitCode = exitCode;
     this.stdout = stdout;
-    this.stderr = stderr;
   }
 
   @Override
   public String getMessage() {
-    return "blaze info failed with exit code: " + exitCode + " and error stream: " + stderr;
-  }
-
-  public int getExitCode() {
-    return exitCode;
+    return "blaze info failed with exit code: " + exitCode;
   }
 
   public String getStdout() {
     return stdout;
   }
-
-  public String getStderr() {
-    return stderr;
-  }
 }
diff --git a/base/src/com/google/idea/blaze/base/command/info/BlazeInfoRunner.java b/base/src/com/google/idea/blaze/base/command/info/BlazeInfoRunner.java
new file mode 100644
index 0000000..fb29f7d
--- /dev/null
+++ b/base/src/com/google/idea/blaze/base/command/info/BlazeInfoRunner.java
@@ -0,0 +1,69 @@
+/*
+ * 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.command.info;
+
+import com.google.common.util.concurrent.ListenableFuture;
+import com.google.idea.blaze.base.model.primitives.WorkspaceRoot;
+import com.google.idea.blaze.base.scope.BlazeContext;
+import com.google.idea.blaze.base.settings.Blaze.BuildSystem;
+import com.intellij.openapi.components.ServiceManager;
+import java.util.List;
+
+/** Runs the blaze info command. The results may be cached in the workspace. */
+public abstract class BlazeInfoRunner {
+
+  public static BlazeInfoRunner getInstance() {
+    return ServiceManager.getService(BlazeInfoRunner.class);
+  }
+
+  /**
+   * @param blazeFlags The blaze flags that will be passed to Blaze.
+   * @param key The key passed to blaze info
+   * @return The blaze info value associated with the specified key
+   */
+  public abstract ListenableFuture<String> runBlazeInfo(
+      BlazeContext context,
+      String binaryPath,
+      WorkspaceRoot workspaceRoot,
+      List<String> blazeFlags,
+      String key);
+
+  /**
+   * @param blazeFlags The blaze flags that will be passed to Blaze.
+   * @param key The key passed to blaze info
+   * @return The blaze info value associated with the specified key
+   */
+  public abstract ListenableFuture<byte[]> runBlazeInfoGetBytes(
+      BlazeContext context,
+      String binaryPath,
+      WorkspaceRoot workspaceRoot,
+      List<String> blazeFlags,
+      String key);
+
+  /**
+   * This calls blaze info without any specific key so blaze info will return all keys and values
+   * that it has.
+   *
+   * @param blazeFlags The blaze flags that will be passed to Blaze.
+   * @return The blaze info data fields.
+   */
+  public abstract ListenableFuture<BlazeInfo> runBlazeInfo(
+      BlazeContext context,
+      BuildSystem buildSystem,
+      String binaryPath,
+      WorkspaceRoot workspaceRoot,
+      List<String> blazeFlags);
+}
diff --git a/base/src/com/google/idea/blaze/base/command/info/BlazeInfoImpl.java b/base/src/com/google/idea/blaze/base/command/info/BlazeInfoRunnerImpl.java
similarity index 74%
rename from base/src/com/google/idea/blaze/base/command/info/BlazeInfoImpl.java
rename to base/src/com/google/idea/blaze/base/command/info/BlazeInfoRunnerImpl.java
index 4b9fe6c..892da90 100644
--- a/base/src/com/google/idea/blaze/base/command/info/BlazeInfoImpl.java
+++ b/base/src/com/google/idea/blaze/base/command/info/BlazeInfoRunnerImpl.java
@@ -19,6 +19,8 @@
 import com.google.common.util.concurrent.ListenableFuture;
 import com.google.idea.blaze.base.async.executor.BlazeExecutor;
 import com.google.idea.blaze.base.async.process.ExternalTask;
+import com.google.idea.blaze.base.async.process.LineProcessingOutputStream;
+import com.google.idea.blaze.base.async.process.PrintOutputLineProcessor;
 import com.google.idea.blaze.base.command.BlazeCommand;
 import com.google.idea.blaze.base.command.BlazeCommandName;
 import com.google.idea.blaze.base.model.primitives.WorkspaceRoot;
@@ -29,77 +31,78 @@
 import java.util.List;
 import javax.annotation.Nullable;
 
-class BlazeInfoImpl extends BlazeInfo {
-  private static final Logger logger = Logger.getInstance(BlazeInfoImpl.class);
+class BlazeInfoRunnerImpl extends BlazeInfoRunner {
+  private static final Logger logger = Logger.getInstance(BlazeInfoRunnerImpl.class);
 
   @Override
   public ListenableFuture<String> runBlazeInfo(
-      @Nullable BlazeContext context,
-      BuildSystem buildSystem,
+      BlazeContext context,
+      String binaryPath,
       WorkspaceRoot workspaceRoot,
       List<String> blazeFlags,
       String key) {
     return BlazeExecutor.getInstance()
         .submit(
             () ->
-                runBlazeInfo(buildSystem, workspaceRoot, key, blazeFlags, context)
+                runBlazeInfo(binaryPath, workspaceRoot, key, blazeFlags, context)
                     .toString()
                     .trim());
   }
 
   @Override
   public ListenableFuture<byte[]> runBlazeInfoGetBytes(
-      @Nullable BlazeContext context,
-      BuildSystem buildSystem,
+      BlazeContext context,
+      String binaryPath,
       WorkspaceRoot workspaceRoot,
       List<String> blazeFlags,
       String key) {
     return BlazeExecutor.getInstance()
         .submit(
-            () -> runBlazeInfo(buildSystem, workspaceRoot, key, blazeFlags, context).toByteArray());
+            () -> runBlazeInfo(binaryPath, workspaceRoot, key, blazeFlags, context).toByteArray());
   }
 
   @Override
-  public ListenableFuture<ImmutableMap<String, String>> runBlazeInfo(
-      @Nullable BlazeContext context,
+  public ListenableFuture<BlazeInfo> runBlazeInfo(
+      BlazeContext context,
       BuildSystem buildSystem,
+      String binaryPath,
       WorkspaceRoot workspaceRoot,
       List<String> blazeFlags) {
     return BlazeExecutor.getInstance()
         .submit(
             () -> {
               String blazeInfoString =
-                  runBlazeInfo(buildSystem, workspaceRoot, null /* key */, blazeFlags, context)
+                  runBlazeInfo(binaryPath, workspaceRoot, /* key */ null, blazeFlags, context)
                       .toString()
                       .trim();
-              return parseBlazeInfoResult(blazeInfoString);
+              ImmutableMap<String, String> blazeInfoMap = parseBlazeInfoResult(blazeInfoString);
+              return new BlazeInfo(buildSystem, blazeInfoMap);
             });
   }
 
   private static ByteArrayOutputStream runBlazeInfo(
-      BuildSystem buildSystem,
+      String binaryPath,
       WorkspaceRoot workspaceRoot,
       @Nullable String key,
       List<String> blazeFlags,
-      @Nullable BlazeContext context)
+      BlazeContext context)
       throws BlazeInfoException {
-    BlazeCommand.Builder builder = BlazeCommand.builder(buildSystem, BlazeCommandName.INFO);
+    BlazeCommand.Builder builder = BlazeCommand.builder(binaryPath, BlazeCommandName.INFO);
     if (key != null) {
       builder.addBlazeFlags(key);
     }
     BlazeCommand command = builder.addBlazeFlags(blazeFlags).build();
     ByteArrayOutputStream stdout = new ByteArrayOutputStream();
-    ByteArrayOutputStream stderr = new ByteArrayOutputStream();
     int exitCode =
         ExternalTask.builder(workspaceRoot)
             .addBlazeCommand(command)
             .context(context)
             .stdout(stdout)
-            .stderr(stderr)
+            .stderr(LineProcessingOutputStream.of(new PrintOutputLineProcessor(context)))
             .build()
             .run();
     if (exitCode != 0) {
-      throw new BlazeInfoException(exitCode, stdout.toString(), stderr.toString());
+      throw new BlazeInfoException(exitCode, stdout.toString());
     }
     return stdout;
   }
diff --git a/base/src/com/google/idea/blaze/base/console/BlazeConsoleService.java b/base/src/com/google/idea/blaze/base/console/BlazeConsoleService.java
index 2bd7d57..2e40a53 100644
--- a/base/src/com/google/idea/blaze/base/console/BlazeConsoleService.java
+++ b/base/src/com/google/idea/blaze/base/console/BlazeConsoleService.java
@@ -18,8 +18,8 @@
 import com.intellij.execution.ui.ConsoleViewContentType;
 import com.intellij.openapi.components.ServiceManager;
 import com.intellij.openapi.project.Project;
+import javax.annotation.Nullable;
 import org.jetbrains.annotations.NotNull;
-import org.jetbrains.annotations.Nullable;
 
 /** Prints text to the blaze console. */
 public interface BlazeConsoleService {
diff --git a/base/src/com/google/idea/blaze/base/console/BlazeConsoleServiceImpl.java b/base/src/com/google/idea/blaze/base/console/BlazeConsoleServiceImpl.java
index ba5e3a6..5b4792b 100644
--- a/base/src/com/google/idea/blaze/base/console/BlazeConsoleServiceImpl.java
+++ b/base/src/com/google/idea/blaze/base/console/BlazeConsoleServiceImpl.java
@@ -19,8 +19,8 @@
 import com.intellij.openapi.project.Project;
 import com.intellij.openapi.wm.ToolWindow;
 import com.intellij.openapi.wm.ToolWindowManager;
+import javax.annotation.Nullable;
 import org.jetbrains.annotations.NotNull;
-import org.jetbrains.annotations.Nullable;
 
 /** Implementation for BlazeConsoleService */
 public class BlazeConsoleServiceImpl implements BlazeConsoleService {
diff --git a/base/src/com/google/idea/blaze/base/console/BlazeConsoleView.java b/base/src/com/google/idea/blaze/base/console/BlazeConsoleView.java
index d5c02c3..58162c5 100644
--- a/base/src/com/google/idea/blaze/base/console/BlazeConsoleView.java
+++ b/base/src/com/google/idea/blaze/base/console/BlazeConsoleView.java
@@ -38,10 +38,10 @@
 import com.intellij.ui.content.Content;
 import com.intellij.ui.content.ContentFactory;
 import java.awt.BorderLayout;
+import javax.annotation.Nullable;
 import javax.swing.JComponent;
 import javax.swing.JPanel;
 import org.jetbrains.annotations.NotNull;
-import org.jetbrains.annotations.Nullable;
 
 class BlazeConsoleView implements Disposable {
 
diff --git a/base/src/com/google/idea/blaze/base/filecache/FileDiffer.java b/base/src/com/google/idea/blaze/base/filecache/FileDiffer.java
index 0142f06..052bb6e 100644
--- a/base/src/com/google/idea/blaze/base/filecache/FileDiffer.java
+++ b/base/src/com/google/idea/blaze/base/filecache/FileDiffer.java
@@ -23,7 +23,7 @@
 import java.util.List;
 import java.util.Map;
 import java.util.Set;
-import org.jetbrains.annotations.Nullable;
+import javax.annotation.Nullable;
 
 /** Provides a diffing service for a collection of files. */
 public final class FileDiffer {
diff --git a/base/src/com/google/idea/blaze/base/ide/NewBlazePackageDialog.java b/base/src/com/google/idea/blaze/base/ide/NewBlazePackageDialog.java
index 1f5d7c5..7317f31 100644
--- a/base/src/com/google/idea/blaze/base/ide/NewBlazePackageDialog.java
+++ b/base/src/com/google/idea/blaze/base/ide/NewBlazePackageDialog.java
@@ -38,11 +38,11 @@
 import java.awt.GridBagLayout;
 import java.io.File;
 import java.util.List;
+import javax.annotation.Nullable;
 import javax.swing.Icon;
 import javax.swing.JComponent;
 import javax.swing.JPanel;
 import org.jetbrains.annotations.NotNull;
-import org.jetbrains.annotations.Nullable;
 
 class NewBlazePackageDialog extends DialogWrapper {
   private static final Logger logger = Logger.getInstance(NewBlazePackageDialog.class);
@@ -112,7 +112,7 @@
     WorkspacePath newPackagePath = workspaceRoot.workspacePathFor(newPackageDirectory);
 
     TargetName newTargetName = newRuleUI.getRuleName();
-    Label newRule = new Label(newPackagePath, newTargetName);
+    Label newRule = Label.create(newPackagePath, newTargetName);
     Kind ruleKind = newRuleUI.getSelectedRuleKind();
     try {
       parentDirectory.checkCreateSubdirectory(newPackageName);
diff --git a/base/src/com/google/idea/blaze/base/ide/NewBlazeRuleDialog.java b/base/src/com/google/idea/blaze/base/ide/NewBlazeRuleDialog.java
index d77e25c..ae43540 100644
--- a/base/src/com/google/idea/blaze/base/ide/NewBlazeRuleDialog.java
+++ b/base/src/com/google/idea/blaze/base/ide/NewBlazeRuleDialog.java
@@ -24,7 +24,6 @@
 import com.google.idea.blaze.base.scope.BlazeContext;
 import com.google.idea.blaze.base.settings.Blaze;
 import com.google.idea.blaze.base.ui.UiUtil;
-import com.intellij.openapi.diagnostic.Logger;
 import com.intellij.openapi.project.Project;
 import com.intellij.openapi.ui.DialogWrapper;
 import com.intellij.openapi.ui.ValidationInfo;
@@ -37,8 +36,6 @@
 import javax.swing.JPanel;
 
 class NewBlazeRuleDialog extends DialogWrapper {
-  private static final Logger logger = Logger.getInstance(NewBlazeRuleDialog.class);
-
   private static final int UI_INDENT = 0;
   private static final int TEXT_BOX_WIDTH = 40;
 
@@ -94,7 +91,7 @@
     WorkspaceRoot workspaceRoot = WorkspaceRoot.fromProject(project);
     WorkspacePath workspacePath =
         workspaceRoot.workspacePathFor(new File(buildFile.getParent().getPath()));
-    Label newRule = new Label(workspacePath, targetName);
+    Label newRule = Label.create(workspacePath, targetName);
     BuildFileModifier buildFileModifier = BuildFileModifier.getInstance();
     boolean success = buildFileModifier.addRule(project, context, newRule, ruleKind);
 
diff --git a/base/src/com/google/idea/blaze/base/ide/NewRuleUI.java b/base/src/com/google/idea/blaze/base/ide/NewRuleUI.java
index 8f859fd..bf0c26a 100644
--- a/base/src/com/google/idea/blaze/base/ide/NewRuleUI.java
+++ b/base/src/com/google/idea/blaze/base/ide/NewRuleUI.java
@@ -27,9 +27,9 @@
 import com.intellij.ui.components.JBTextField;
 import java.util.Collection;
 import java.util.List;
+import javax.annotation.Nullable;
 import javax.swing.JPanel;
 import org.jetbrains.annotations.NotNull;
-import org.jetbrains.annotations.Nullable;
 
 final class NewRuleUI {
 
diff --git a/base/src/com/google/idea/blaze/base/ide/OpenBlazeWorkspaceFileAction.java b/base/src/com/google/idea/blaze/base/ide/OpenBlazeWorkspaceFileAction.java
new file mode 100644
index 0000000..68976c8
--- /dev/null
+++ b/base/src/com/google/idea/blaze/base/ide/OpenBlazeWorkspaceFileAction.java
@@ -0,0 +1,108 @@
+/*
+ * Copyright 2017 The Bazel Authors. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.idea.blaze.base.ide;
+
+import com.google.idea.blaze.base.actions.BlazeProjectAction;
+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.WorkspacePathResolver;
+import com.google.idea.blaze.base.ui.UiUtil;
+import com.google.idea.blaze.base.ui.WorkspaceFileTextField;
+import com.intellij.ide.actions.OpenFileAction;
+import com.intellij.openapi.actionSystem.AnActionEvent;
+import com.intellij.openapi.fileChooser.FileChooserDescriptor;
+import com.intellij.openapi.fileChooser.FileChooserDescriptorFactory;
+import com.intellij.openapi.fileChooser.FileTextField;
+import com.intellij.openapi.project.Project;
+import com.intellij.openapi.ui.DialogWrapper;
+import com.intellij.openapi.ui.ValidationInfo;
+import com.intellij.openapi.vfs.VirtualFile;
+import com.intellij.ui.components.JBLabel;
+import java.awt.GridBagLayout;
+import javax.annotation.Nullable;
+import javax.swing.JComponent;
+import javax.swing.JPanel;
+
+final class OpenBlazeWorkspaceFileAction extends BlazeProjectAction {
+
+  @Override
+  protected void actionPerformedInBlazeProject(Project project, AnActionEvent e) {
+    BlazeProjectData blazeProjectData =
+        BlazeProjectDataManager.getInstance(project).getBlazeProjectData();
+    if (blazeProjectData == null) {
+      return;
+    }
+    new OpenBlazeWorkspaceFileActionDialog(project, blazeProjectData.workspacePathResolver).show();
+  }
+
+  private static class OpenBlazeWorkspaceFileActionDialog extends DialogWrapper {
+
+    static final int PATH_FIELD_WIDTH = 40;
+    final Project project;
+
+    final JPanel component;
+    final FileTextField fileTextField;
+
+    OpenBlazeWorkspaceFileActionDialog(
+        Project project, WorkspacePathResolver workspacePathResolver) {
+      super(project, /* canBeParent */ false, IdeModalityType.PROJECT);
+      this.project = project;
+
+      component = new JPanel(new GridBagLayout());
+      FileChooserDescriptor descriptor = FileChooserDescriptorFactory.createSingleFileDescriptor();
+      fileTextField =
+          WorkspaceFileTextField.create(
+              workspacePathResolver, descriptor, PATH_FIELD_WIDTH, myDisposable);
+
+      component.add(new JBLabel("Path:"));
+      component.add(fileTextField.getField(), UiUtil.getFillLineConstraints(0));
+
+      UiUtil.fillBottom(component);
+      init();
+    }
+
+    @Nullable
+    @Override
+    protected JComponent createCenterPanel() {
+      return component;
+    }
+
+    @Nullable
+    @Override
+    public JComponent getPreferredFocusedComponent() {
+      return fileTextField.getField();
+    }
+
+    @Nullable
+    @Override
+    protected ValidationInfo doValidate() {
+      VirtualFile selectedFile = fileTextField.getSelectedFile();
+      if (selectedFile == null || !selectedFile.exists()) {
+        return new ValidationInfo("File does not exist", fileTextField.getField());
+      } else if (selectedFile.isDirectory()) {
+        return new ValidationInfo("Directories can not be opened", fileTextField.getField());
+      } else {
+        return null;
+      }
+    }
+
+    @Override
+    protected void doOKAction() {
+      OpenFileAction.openFile(fileTextField.getSelectedFile(), project);
+      super.doOKAction();
+    }
+  }
+}
diff --git a/base/src/com/google/idea/blaze/base/ideinfo/AndroidIdeInfo.java b/base/src/com/google/idea/blaze/base/ideinfo/AndroidIdeInfo.java
index 760fac7..68a7565 100644
--- a/base/src/com/google/idea/blaze/base/ideinfo/AndroidIdeInfo.java
+++ b/base/src/com/google/idea/blaze/base/ideinfo/AndroidIdeInfo.java
@@ -19,7 +19,7 @@
 import com.google.idea.blaze.base.model.primitives.Label;
 import java.io.Serializable;
 import java.util.Collection;
-import org.jetbrains.annotations.Nullable;
+import javax.annotation.Nullable;
 
 /** Ide info specific to android rules. */
 public final class AndroidIdeInfo implements Serializable {
diff --git a/base/src/com/google/idea/blaze/base/run/testlogs/CompletedTestTarget.java b/base/src/com/google/idea/blaze/base/ideinfo/AndroidSdkIdeInfo.java
similarity index 60%
rename from base/src/com/google/idea/blaze/base/run/testlogs/CompletedTestTarget.java
rename to base/src/com/google/idea/blaze/base/ideinfo/AndroidSdkIdeInfo.java
index bf0b33b..1a07184 100644
--- a/base/src/com/google/idea/blaze/base/run/testlogs/CompletedTestTarget.java
+++ b/base/src/com/google/idea/blaze/base/ideinfo/AndroidSdkIdeInfo.java
@@ -13,19 +13,17 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package com.google.idea.blaze.base.run.testlogs;
+package com.google.idea.blaze.base.ideinfo;
 
-import com.google.idea.blaze.base.model.primitives.Label;
-import java.io.File;
+import java.io.Serializable;
 
-/** Information relating to a completed test target. */
-public class CompletedTestTarget {
+/** android_sdk ide info */
+public class AndroidSdkIdeInfo implements Serializable {
+  private static final long serialVersionUID = 1L;
 
-  public final File testResultXml;
-  public final Label label;
+  public final ArtifactLocation androidJar;
 
-  public CompletedTestTarget(File testResultXml, Label label) {
-    this.testResultXml = testResultXml;
-    this.label = label;
+  public AndroidSdkIdeInfo(ArtifactLocation androidJar) {
+    this.androidJar = androidJar;
   }
 }
diff --git a/base/src/com/google/idea/blaze/base/ideinfo/ArtifactLocation.java b/base/src/com/google/idea/blaze/base/ideinfo/ArtifactLocation.java
index a23ab9c..72dc07c 100644
--- a/base/src/com/google/idea/blaze/base/ideinfo/ArtifactLocation.java
+++ b/base/src/com/google/idea/blaze/base/ideinfo/ArtifactLocation.java
@@ -22,9 +22,9 @@
 
 /** Represents a blaze-produced artifact. */
 public final class ArtifactLocation implements Serializable, Comparable<ArtifactLocation> {
-  private static final long serialVersionUID = 4L;
+  private static final long serialVersionUID = 5L;
 
-  public final String rootExecutionPathFragment;
+  private final String rootExecutionPathFragment;
   public final String relativePath;
   public final boolean isSource;
   public final boolean isExternal;
@@ -37,7 +37,10 @@
     this.isExternal = isExternal;
   }
 
-  /** Gets the path relative to the root path. */
+  /**
+   * The root-relative path. For external workspace artifacts, this is relative to the external
+   * workspace root.
+   */
   public String getRelativePath() {
     return relativePath;
   }
@@ -50,10 +53,7 @@
     return !isSource;
   }
 
-  /**
-   * Returns rootExecutionPathFragment + relativePath. For source artifacts, this is simply
-   * relativePath
-   */
+  /** For main-workspace source artifacts, this is simply the workspace-relative path. */
   public String getExecutionRootRelativePath() {
     return Paths.get(rootExecutionPathFragment, relativePath).toString();
   }
@@ -124,8 +124,8 @@
     return ComparisonChain.start()
         .compare(rootExecutionPathFragment, o.rootExecutionPathFragment)
         .compare(relativePath, o.relativePath)
-        .compare(isSource, o.isSource)
-        .compare(isExternal, o.isExternal)
+        .compareFalseFirst(isSource, o.isSource)
+        .compareFalseFirst(isExternal, o.isExternal)
         .result();
   }
 }
diff --git a/base/src/com/google/idea/blaze/base/ideinfo/CIdeInfo.java b/base/src/com/google/idea/blaze/base/ideinfo/CIdeInfo.java
index de80f8c..215033d 100644
--- a/base/src/com/google/idea/blaze/base/ideinfo/CIdeInfo.java
+++ b/base/src/com/google/idea/blaze/base/ideinfo/CIdeInfo.java
@@ -21,10 +21,12 @@
 
 /** Sister class to {@link JavaIdeInfo} */
 public class CIdeInfo implements Serializable {
-  private static final long serialVersionUID = 6L;
+  private static final long serialVersionUID = 7L;
 
   public final ImmutableList<ArtifactLocation> sources;
 
+  public final ImmutableList<String> localDefines;
+  public final ImmutableList<ExecutionRootPath> localIncludeDirectories;
   // From the cpp compilation context provider.
   // These should all be for the entire transitive closure.
   public final ImmutableList<ExecutionRootPath> transitiveIncludeDirectories;
@@ -34,11 +36,15 @@
 
   public CIdeInfo(
       ImmutableList<ArtifactLocation> sources,
+      ImmutableList<String> localDefines,
+      ImmutableList<ExecutionRootPath> localIncludeDirectories,
       ImmutableList<ExecutionRootPath> transitiveIncludeDirectories,
       ImmutableList<ExecutionRootPath> transitiveQuoteIncludeDirectories,
       ImmutableList<String> transitiveDefines,
       ImmutableList<ExecutionRootPath> transitiveSystemIncludeDirectories) {
     this.sources = sources;
+    this.localDefines = localDefines;
+    this.localIncludeDirectories = localIncludeDirectories;
     this.transitiveIncludeDirectories = transitiveIncludeDirectories;
     this.transitiveQuoteIncludeDirectories = transitiveQuoteIncludeDirectories;
     this.transitiveDefines = transitiveDefines;
@@ -53,6 +59,9 @@
   public static class Builder {
     private final ImmutableList.Builder<ArtifactLocation> sources = ImmutableList.builder();
 
+    private final ImmutableList.Builder<String> localDefines = ImmutableList.builder();
+    private final ImmutableList.Builder<ExecutionRootPath> localIncludeDirectories =
+        ImmutableList.builder();
     private final ImmutableList.Builder<ExecutionRootPath> transitiveIncludeDirectories =
         ImmutableList.builder();
     private final ImmutableList.Builder<ExecutionRootPath> transitiveQuoteIncludeDirectories =
@@ -66,6 +75,16 @@
       return this;
     }
 
+    public Builder addLocalDefines(Iterable<String> localDefines) {
+      this.localDefines.addAll(localDefines);
+      return this;
+    }
+
+    public Builder addLocalIncludeDirectories(Iterable<ExecutionRootPath> localIncludeDirectories) {
+      this.localIncludeDirectories.addAll(localIncludeDirectories);
+      return this;
+    }
+
     public Builder addTransitiveIncludeDirectories(
         Iterable<ExecutionRootPath> transitiveIncludeDirectories) {
       this.transitiveIncludeDirectories.addAll(transitiveIncludeDirectories);
@@ -92,6 +111,8 @@
     public CIdeInfo build() {
       return new CIdeInfo(
           sources.build(),
+          localDefines.build(),
+          localIncludeDirectories.build(),
           transitiveIncludeDirectories.build(),
           transitiveQuoteIncludeDirectories.build(),
           transitiveDefines.build(),
@@ -106,6 +127,12 @@
         + "  sources="
         + sources
         + "\n"
+        + "  localDefines="
+        + localDefines
+        + "\n"
+        + "  localIncludeDirectories="
+        + localIncludeDirectories
+        + "\n"
         + "  transitiveIncludeDirectories="
         + transitiveIncludeDirectories
         + "\n"
diff --git a/base/src/com/google/idea/blaze/base/ideinfo/CToolchainIdeInfo.java b/base/src/com/google/idea/blaze/base/ideinfo/CToolchainIdeInfo.java
index f1c0174..5224256 100644
--- a/base/src/com/google/idea/blaze/base/ideinfo/CToolchainIdeInfo.java
+++ b/base/src/com/google/idea/blaze/base/ideinfo/CToolchainIdeInfo.java
@@ -20,7 +20,7 @@
 import com.google.idea.blaze.base.model.primitives.ExecutionRootPath;
 import java.io.Serializable;
 
-/** Sister class to {@link JavaIdeInfo} */
+/** Represents a cc_toolchain */
 public class CToolchainIdeInfo implements Serializable {
   private static final long serialVersionUID = 3L;
 
diff --git a/base/src/com/google/idea/blaze/base/ideinfo/IntellijPluginDeployInfo.java b/base/src/com/google/idea/blaze/base/ideinfo/IntellijPluginDeployInfo.java
deleted file mode 100644
index 426fa6b..0000000
--- a/base/src/com/google/idea/blaze/base/ideinfo/IntellijPluginDeployInfo.java
+++ /dev/null
@@ -1,48 +0,0 @@
-/*
- * Copyright 2017 The Bazel Authors. All rights reserved.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *    http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.google.idea.blaze.base.ideinfo;
-
-import com.google.common.collect.ImmutableList;
-import java.io.Serializable;
-import javax.annotation.concurrent.Immutable;
-
-/** A special rule representing the files that need to be deployed for an IntelliJ plugin */
-@Immutable
-public class IntellijPluginDeployInfo implements Serializable {
-  private static final long serialVersionUID = 1L;
-
-  /** A single file for deployment */
-  @Immutable
-  public static class IntellijPluginDeployFile implements Serializable {
-    private static final long serialVersionUID = 1L;
-
-    /** The source file to deploy. */
-    public final ArtifactLocation src;
-    /** A plugins-directory relative location to deploy to. */
-    public final String deployLocation;
-
-    public IntellijPluginDeployFile(ArtifactLocation src, String deployLocation) {
-      this.src = src;
-      this.deployLocation = deployLocation;
-    }
-  }
-
-  public final ImmutableList<IntellijPluginDeployFile> deployFiles;
-
-  public IntellijPluginDeployInfo(ImmutableList<IntellijPluginDeployFile> deployFiles) {
-    this.deployFiles = deployFiles;
-  }
-}
diff --git a/base/src/com/google/idea/blaze/base/ideinfo/JavaIdeInfo.java b/base/src/com/google/idea/blaze/base/ideinfo/JavaIdeInfo.java
index 174db51..271e390 100644
--- a/base/src/com/google/idea/blaze/base/ideinfo/JavaIdeInfo.java
+++ b/base/src/com/google/idea/blaze/base/ideinfo/JavaIdeInfo.java
@@ -18,7 +18,7 @@
 import com.google.common.collect.ImmutableList;
 import java.io.Serializable;
 import java.util.Collection;
-import org.jetbrains.annotations.Nullable;
+import javax.annotation.Nullable;
 
 /** Ide info specific to java rules. */
 public final class JavaIdeInfo implements Serializable {
diff --git a/base/src/com/google/idea/blaze/base/ideinfo/LibraryArtifact.java b/base/src/com/google/idea/blaze/base/ideinfo/LibraryArtifact.java
index f661e5a..6eaab56 100644
--- a/base/src/com/google/idea/blaze/base/ideinfo/LibraryArtifact.java
+++ b/base/src/com/google/idea/blaze/base/ideinfo/LibraryArtifact.java
@@ -17,7 +17,7 @@
 
 import com.google.common.base.Objects;
 import java.io.Serializable;
-import org.jetbrains.annotations.Nullable;
+import javax.annotation.Nullable;
 
 /** Represents a jar artifact. */
 public class LibraryArtifact implements Serializable {
diff --git a/base/src/com/google/idea/blaze/base/ideinfo/TargetIdeInfo.java b/base/src/com/google/idea/blaze/base/ideinfo/TargetIdeInfo.java
index 34f2bc2..ecd44c5 100644
--- a/base/src/com/google/idea/blaze/base/ideinfo/TargetIdeInfo.java
+++ b/base/src/com/google/idea/blaze/base/ideinfo/TargetIdeInfo.java
@@ -27,7 +27,7 @@
 
 /** Simple implementation of TargetIdeInfo. */
 public final class TargetIdeInfo implements Serializable {
-  private static final long serialVersionUID = 15L;
+  private static final long serialVersionUID = 17L;
 
   public final TargetKey key;
   public final Kind kind;
@@ -39,11 +39,11 @@
   @Nullable public final CToolchainIdeInfo cToolchainIdeInfo;
   @Nullable public final JavaIdeInfo javaIdeInfo;
   @Nullable public final AndroidIdeInfo androidIdeInfo;
+  @Nullable public final AndroidSdkIdeInfo androidSdkIdeInfo;
   @Nullable public final PyIdeInfo pyIdeInfo;
   @Nullable public final TestIdeInfo testIdeInfo;
   @Nullable public final ProtoLibraryLegacyInfo protoLibraryLegacyInfo;
   @Nullable public final JavaToolchainIdeInfo javaToolchainIdeInfo;
-  @Nullable public final IntellijPluginDeployInfo intellijPluginDeployInfo;
 
   public TargetIdeInfo(
       TargetKey key,
@@ -56,11 +56,11 @@
       @Nullable CToolchainIdeInfo cToolchainIdeInfo,
       @Nullable JavaIdeInfo javaIdeInfo,
       @Nullable AndroidIdeInfo androidIdeInfo,
+      @Nullable AndroidSdkIdeInfo androidSdkIdeInfo,
       @Nullable PyIdeInfo pyIdeInfo,
       @Nullable TestIdeInfo testIdeInfo,
       @Nullable ProtoLibraryLegacyInfo protoLibraryLegacyInfo,
-      @Nullable JavaToolchainIdeInfo javaToolchainIdeInfo,
-      @Nullable IntellijPluginDeployInfo intellijPluginDeployInfo) {
+      @Nullable JavaToolchainIdeInfo javaToolchainIdeInfo) {
     this.key = key;
     this.kind = kind;
     this.buildFile = buildFile;
@@ -71,11 +71,11 @@
     this.cToolchainIdeInfo = cToolchainIdeInfo;
     this.javaIdeInfo = javaIdeInfo;
     this.androidIdeInfo = androidIdeInfo;
+    this.androidSdkIdeInfo = androidSdkIdeInfo;
     this.pyIdeInfo = pyIdeInfo;
     this.testIdeInfo = testIdeInfo;
     this.protoLibraryLegacyInfo = protoLibraryLegacyInfo;
     this.javaToolchainIdeInfo = javaToolchainIdeInfo;
-    this.intellijPluginDeployInfo = intellijPluginDeployInfo;
   }
 
   @Override
@@ -89,7 +89,7 @@
   }
 
   /** Returns whether this rule is one of the kinds. */
-  public boolean kindIsOneOf(List<Kind> kinds) {
+  public boolean kindIsOneOf(Collection<Kind> kinds) {
     if (kind != null) {
       return kind.isOneOf(kinds);
     }
@@ -122,7 +122,7 @@
     private JavaToolchainIdeInfo javaToolchainIdeInfo;
 
     public Builder setLabel(String label) {
-      return setLabel(new Label(label));
+      return setLabel(Label.create(label));
     }
 
     public Builder setLabel(Label label) {
@@ -212,7 +212,7 @@
     }
 
     public Builder addDependency(String s) {
-      return addDependency(new Label(s));
+      return addDependency(Label.create(s));
     }
 
     public Builder addDependency(Label label) {
@@ -222,7 +222,7 @@
     }
 
     public Builder addRuntimeDep(String s) {
-      return addRuntimeDep(new Label(s));
+      return addRuntimeDep(Label.create(s));
     }
 
     public Builder addRuntimeDep(Label label) {
@@ -243,11 +243,11 @@
           cToolchainIdeInfo,
           javaIdeInfo,
           androidIdeInfo,
+          null,
           pyIdeInfo,
           testIdeInfo,
           protoLibraryLegacyInfo,
-          javaToolchainIdeInfo,
-          null);
+          javaToolchainIdeInfo);
     }
   }
 }
diff --git a/base/src/com/google/idea/blaze/base/ideinfo/TargetKey.java b/base/src/com/google/idea/blaze/base/ideinfo/TargetKey.java
index d1e99a1..9803427 100644
--- a/base/src/com/google/idea/blaze/base/ideinfo/TargetKey.java
+++ b/base/src/com/google/idea/blaze/base/ideinfo/TargetKey.java
@@ -24,7 +24,7 @@
 import java.io.Serializable;
 import java.util.List;
 
-/** A key that uniquely idenfifies a target in the target map */
+/** A key that uniquely identifies a target in the target map */
 public class TargetKey implements Serializable, Comparable<TargetKey> {
   private static final long serialVersionUID = 3L;
 
diff --git a/base/src/com/google/idea/blaze/base/issueparser/BlazeIssueParser.java b/base/src/com/google/idea/blaze/base/issueparser/BlazeIssueParser.java
index 2ebd868..55755ec 100644
--- a/base/src/com/google/idea/blaze/base/issueparser/BlazeIssueParser.java
+++ b/base/src/com/google/idea/blaze/base/issueparser/BlazeIssueParser.java
@@ -36,11 +36,11 @@
 import java.util.regex.Pattern;
 import javax.annotation.Nullable;
 
-
 /** Parses blaze output for compile errors. */
 public class BlazeIssueParser {
 
-  private static class ParseResult {
+  /** Result from parsing the current line */
+  public static class ParseResult {
 
     public static final ParseResult NEEDS_MORE_INPUT = new ParseResult(true, null);
 
@@ -117,9 +117,9 @@
 
   /** Returns the file referenced by the target */
   @Nullable
-  public static File fileFromTarget(WorkspaceRoot workspaceRoot, String targetString) {
+  private static File fileFromTarget(WorkspaceRoot workspaceRoot, String targetString) {
     Label label = Label.createIfValid(targetString);
-    if (label == null) {
+    if (label == null || label.isExternal()) {
       return null;
     }
     try {
@@ -132,7 +132,10 @@
   }
 
   /** Falls back to returning -1 if no integer can be parsed. */
-  public static int parseOptionalInt(String intString) {
+  public static int parseOptionalInt(@Nullable String intString) {
+    if (intString == null) {
+      return -1;
+    }
     try {
       return Integer.parseInt(intString);
     } catch (NumberFormatException e) {
@@ -215,6 +218,22 @@
     }
   }
 
+  static class SkylarkErrorParser extends SingleLineParser {
+    SkylarkErrorParser() {
+      super("^ERROR: (/.*?\\.bzl):([0-9]+):([0-9]+): (.*)$");
+    }
+
+    @Override
+    protected IssueOutput createIssue(Matcher matcher) {
+      File file = fileFromAbsolutePath(matcher.group(1));
+      return IssueOutput.error(matcher.group(4))
+          .inFile(file)
+          .onLine(Integer.parseInt(matcher.group(2)))
+          .inColumn(parseOptionalInt(matcher.group(3)))
+          .build();
+    }
+  }
+
   static class LinelessBuildParser extends SingleLineParser {
     LinelessBuildParser() {
       super("^ERROR: (.*?):char offsets [0-9]+--[0-9]+: (.*)$");
diff --git a/base/src/com/google/idea/blaze/base/issueparser/IssueOutputLineProcessor.java b/base/src/com/google/idea/blaze/base/issueparser/IssueOutputLineProcessor.java
index c3c5de2..a5588dc 100644
--- a/base/src/com/google/idea/blaze/base/issueparser/IssueOutputLineProcessor.java
+++ b/base/src/com/google/idea/blaze/base/issueparser/IssueOutputLineProcessor.java
@@ -49,6 +49,7 @@
             new BlazeIssueParser.CompileParser(workspaceRoot),
             new BlazeIssueParser.TracebackParser(),
             new BlazeIssueParser.BuildParser(),
+            new BlazeIssueParser.SkylarkErrorParser(),
             new BlazeIssueParser.LinelessBuildParser(),
             new BlazeIssueParser.ProjectViewLabelParser(projectViewSet),
             new BlazeIssueParser.InvalidTargetProjectViewPackageParser(
diff --git a/base/src/com/google/idea/blaze/base/lang/AdditionalLanguagesHelper.java b/base/src/com/google/idea/blaze/base/lang/AdditionalLanguagesHelper.java
index 384e35b..a55996c 100644
--- a/base/src/com/google/idea/blaze/base/lang/AdditionalLanguagesHelper.java
+++ b/base/src/com/google/idea/blaze/base/lang/AdditionalLanguagesHelper.java
@@ -15,6 +15,7 @@
  */
 package com.google.idea.blaze.base.lang;
 
+import com.google.common.collect.ImmutableList;
 import com.google.common.collect.Sets;
 import com.google.idea.blaze.base.model.BlazeProjectData;
 import com.google.idea.blaze.base.model.primitives.LanguageClass;
@@ -35,6 +36,7 @@
 import com.intellij.openapi.vfs.VirtualFile;
 import com.intellij.ui.EditorNotificationPanel;
 import com.intellij.ui.EditorNotifications;
+import java.util.List;
 import java.util.Set;
 import javax.annotation.Nullable;
 
@@ -111,12 +113,15 @@
     panel.setText(message);
     panel.createActionLabel(
         String.format("Enable %s support", langName),
-        () -> enableLanguageSupport(project, language));
+        () -> {
+          enableLanguageSupport(project, ImmutableList.of(language));
+          suppressNotifications(language);
+        });
     panel.createActionLabel("Don't show again", () -> suppressNotifications(language));
     return panel;
   }
 
-  private void enableLanguageSupport(Project project, LanguageClass language) {
+  public static void enableLanguageSupport(Project project, List<LanguageClass> languages) {
     ProjectViewEdit edit =
         ProjectViewEdit.editLocalProjectView(
             project,
@@ -126,7 +131,7 @@
               builder.replace(
                   existingSection,
                   ListSection.update(AdditionalLanguagesSection.KEY, existingSection)
-                      .add(language));
+                      .addAll(languages));
               return true;
             });
     if (edit == null) {
@@ -137,8 +142,6 @@
     }
     edit.apply();
 
-    suppressNotifications(language);
-
     BlazeSyncManager.getInstance(project)
         .requestProjectSync(
             new BlazeSyncParams.Builder("Sync", BlazeSyncParams.SyncMode.INCREMENTAL)
diff --git a/base/src/com/google/idea/blaze/base/lang/buildfile/completion/BuildLookupElement.java b/base/src/com/google/idea/blaze/base/lang/buildfile/completion/BuildLookupElement.java
index cc05ea4..cd01e0d 100644
--- a/base/src/com/google/idea/blaze/base/lang/buildfile/completion/BuildLookupElement.java
+++ b/base/src/com/google/idea/blaze/base/lang/buildfile/completion/BuildLookupElement.java
@@ -16,6 +16,7 @@
 package com.google.idea.blaze.base.lang.buildfile.completion;
 
 import com.google.idea.blaze.base.lang.buildfile.references.QuoteType;
+import com.intellij.codeInsight.CodeInsightSettings;
 import com.intellij.codeInsight.completion.InsertionContext;
 import com.intellij.codeInsight.lookup.LookupElement;
 import com.intellij.codeInsight.lookup.LookupElementPresentation;
@@ -42,9 +43,15 @@
     this.wrapWithQuotes = quoteWrapping != QuoteType.NoQuotes;
   }
 
+  private static boolean insertClosingQuotes() {
+    return CodeInsightSettings.getInstance().AUTOINSERT_PAIR_QUOTE;
+  }
+
   @Override
   public String getLookupString() {
-    return quoteWrapping.wrap(baseName);
+    return insertClosingQuotes()
+        ? quoteWrapping.wrap(baseName)
+        : quoteWrapping.quoteString + baseName;
   }
 
   @Nullable
@@ -75,19 +82,17 @@
   /**
    * If we're wrapping with quotes, handle the (very common) case where we have a closing quote
    * after the caret -- we want to remove this quote.
-   *
-   * @param context
    */
   @Override
   public void handleInsert(InsertionContext context) {
-    if (!wrapWithQuotes) {
+    if (!wrapWithQuotes || !insertClosingQuotes()) {
       super.handleInsert(context);
       return;
     }
     Document document = context.getDocument();
     context.commitDocument();
     PsiElement suffix = context.getFile().findElementAt(context.getTailOffset());
-    if (suffix.getText().startsWith(quoteWrapping.quoteString)) {
+    if (suffix != null && suffix.getText().startsWith(quoteWrapping.quoteString)) {
       int offset = suffix.getTextOffset();
       document.deleteString(offset, offset + 1);
       context.commitDocument();
diff --git a/base/src/com/google/idea/blaze/base/lang/buildfile/completion/BuiltInFunctionAttributeCompletionContributor.java b/base/src/com/google/idea/blaze/base/lang/buildfile/completion/BuiltInFunctionAttributeCompletionContributor.java
index 1b16003..1d84632 100644
--- a/base/src/com/google/idea/blaze/base/lang/buildfile/completion/BuiltInFunctionAttributeCompletionContributor.java
+++ b/base/src/com/google/idea/blaze/base/lang/buildfile/completion/BuiltInFunctionAttributeCompletionContributor.java
@@ -96,7 +96,7 @@
 
   @Nullable
   private static String getEnclosingFuncallName(PsiElement element) {
-    FuncallExpression funcall = PsiUtils.getParentOfType(element, FuncallExpression.class);
+    FuncallExpression funcall = PsiUtils.getParentOfType(element, FuncallExpression.class, true);
     return funcall != null ? funcall.getFunctionName() : null;
   }
 }
diff --git a/base/src/com/google/idea/blaze/base/lang/buildfile/completion/CompletionResultsProcessor.java b/base/src/com/google/idea/blaze/base/lang/buildfile/completion/CompletionResultsProcessor.java
index c5a32e5..b9e6172 100644
--- a/base/src/com/google/idea/blaze/base/lang/buildfile/completion/CompletionResultsProcessor.java
+++ b/base/src/com/google/idea/blaze/base/lang/buildfile/completion/CompletionResultsProcessor.java
@@ -32,10 +32,13 @@
   private final Map<String, LookupElement> results = Maps.newHashMap();
   private final PsiElement originalElement;
   private final QuoteType quoteType;
+  private final boolean allowPrivateSymbols;
 
-  public CompletionResultsProcessor(PsiElement originalElement, QuoteType quoteType) {
+  public CompletionResultsProcessor(
+      PsiElement originalElement, QuoteType quoteType, boolean allowPrivateSymbols) {
     this.originalElement = originalElement;
     this.quoteType = quoteType;
+    this.allowPrivateSymbols = allowPrivateSymbols;
   }
 
   @Override
@@ -49,9 +52,11 @@
       results.put(string, new LoadedSymbolReferenceLookupElement(loadedSymbol, string, quoteType));
     } else if (buildElement instanceof PsiNamedElement) {
       PsiNamedElement namedElement = (PsiNamedElement) buildElement;
-      results.put(
-          namedElement.getName(),
-          new NamedBuildLookupElement((PsiNamedElement) buildElement, quoteType));
+      String name = namedElement.getName();
+      if (!allowPrivateSymbols && name != null && name.startsWith("_")) {
+        return true;
+      }
+      results.put(name, new NamedBuildLookupElement((PsiNamedElement) buildElement, quoteType));
     }
     return true;
   }
diff --git a/base/src/com/google/idea/blaze/base/lang/buildfile/completion/FilePathLookupElement.java b/base/src/com/google/idea/blaze/base/lang/buildfile/completion/FilePathLookupElement.java
index 61d40e9..0a5a958 100644
--- a/base/src/com/google/idea/blaze/base/lang/buildfile/completion/FilePathLookupElement.java
+++ b/base/src/com/google/idea/blaze/base/lang/buildfile/completion/FilePathLookupElement.java
@@ -20,7 +20,7 @@
 import javax.annotation.Nullable;
 import javax.swing.Icon;
 
-/** Code completion support for package paths. */
+/** Code completion support for file paths within BUILD file labels. */
 public class FilePathLookupElement extends BuildLookupElement {
 
   private final String itemText;
diff --git a/base/src/com/google/idea/blaze/base/lang/buildfile/completion/FilterPatterns.java b/base/src/com/google/idea/blaze/base/lang/buildfile/completion/FilterPatterns.java
deleted file mode 100644
index d888efb..0000000
--- a/base/src/com/google/idea/blaze/base/lang/buildfile/completion/FilterPatterns.java
+++ /dev/null
@@ -1,46 +0,0 @@
-/*
- * Copyright 2016 The Bazel Authors. All rights reserved.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *    http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.google.idea.blaze.base.lang.buildfile.completion;
-
-import static com.intellij.patterns.PlatformPatterns.psiElement;
-
-import com.intellij.patterns.ElementPattern;
-import com.intellij.patterns.PatternCondition;
-import com.intellij.psi.PsiElement;
-import com.intellij.util.ProcessingContext;
-import org.jetbrains.annotations.NotNull;
-
-/** Filter patterns used by completion contributors. */
-public class FilterPatterns {
-
-  public static final ElementPattern<PsiElement> indexInParentsChildren(final int childIndex) {
-    return psiElement()
-        .with(
-            new PatternCondition<PsiElement>("isIndexInParentsChildren") {
-              @Override
-              public boolean accepts(@NotNull PsiElement psiElement, ProcessingContext context) {
-                final PsiElement parent = psiElement.getParent();
-                if (parent != null) {
-                  final PsiElement[] children = parent.getChildren();
-                  if (childIndex < children.length && psiElement.equals(children[childIndex])) {
-                    return true;
-                  }
-                }
-                return false;
-              }
-            });
-  }
-}
diff --git a/base/src/com/google/idea/blaze/base/lang/buildfile/completion/LabelRuleLookupElement.java b/base/src/com/google/idea/blaze/base/lang/buildfile/completion/LabelRuleLookupElement.java
index dc7dc46..0b9af98 100644
--- a/base/src/com/google/idea/blaze/base/lang/buildfile/completion/LabelRuleLookupElement.java
+++ b/base/src/com/google/idea/blaze/base/lang/buildfile/completion/LabelRuleLookupElement.java
@@ -16,8 +16,6 @@
 package com.google.idea.blaze.base.lang.buildfile.completion;
 
 import com.google.common.collect.Lists;
-import com.google.idea.blaze.base.lang.buildfile.language.semantics.BuildLanguageSpec;
-import com.google.idea.blaze.base.lang.buildfile.language.semantics.BuildLanguageSpecProvider;
 import com.google.idea.blaze.base.lang.buildfile.psi.BuildFile;
 import com.google.idea.blaze.base.lang.buildfile.psi.FuncallExpression;
 import com.google.idea.blaze.base.lang.buildfile.references.LabelUtils;
@@ -45,9 +43,6 @@
 
     String ruleFragment = LabelUtils.getRuleComponent(originalString);
     List<BuildLookupElement> lookups = Lists.newArrayList();
-    // TODO: Handle rules generated via functions? (e.g. via blaze sync)
-    BuildLanguageSpec spec =
-        BuildLanguageSpecProvider.getInstance().getLanguageSpec(file.getProject());
     for (FuncallExpression target : file.findChildrenByClass(FuncallExpression.class)) {
       String targetName = target.getName();
       if (targetName == null
@@ -56,7 +51,7 @@
         continue;
       }
       String ruleType = target.getFunctionName();
-      if (ruleType == null || (spec != null && !spec.hasRule(ruleType))) {
+      if (ruleType == null) {
         continue;
       }
       lookups.add(
diff --git a/base/src/com/google/idea/blaze/base/lang/buildfile/documentation/BuildDocumentationProvider.java b/base/src/com/google/idea/blaze/base/lang/buildfile/documentation/BuildDocumentationProvider.java
index 8c96eec..6f1b195 100644
--- a/base/src/com/google/idea/blaze/base/lang/buildfile/documentation/BuildDocumentationProvider.java
+++ b/base/src/com/google/idea/blaze/base/lang/buildfile/documentation/BuildDocumentationProvider.java
@@ -24,6 +24,7 @@
 import com.google.idea.blaze.base.lang.buildfile.psi.FunctionStatement;
 import com.google.idea.blaze.base.lang.buildfile.psi.ParameterList;
 import com.google.idea.blaze.base.lang.buildfile.psi.StringLiteral;
+import com.google.idea.blaze.base.model.primitives.Label;
 import com.google.idea.blaze.base.settings.Blaze;
 import com.intellij.codeInsight.documentation.DocumentationManagerProtocol;
 import com.intellij.lang.documentation.AbstractDocumentationProvider;
@@ -77,11 +78,8 @@
       return;
     }
     BuildFile buildFile = (BuildFile) file;
-    String name = buildFile.getBuildLabel();
-    if (name == null) {
-      // fall back to qualitative description
-      name = buildFile.getPresentableText();
-    }
+    Label label = buildFile.getBuildLabel();
+    String name = label != null ? label.toString() : buildFile.getPresentableText();
     if (linkToFile) {
       builder
           .append("<a href=\"")
diff --git a/base/src/com/google/idea/blaze/base/lang/buildfile/findusages/BuildReadWriteAccessDetector.java b/base/src/com/google/idea/blaze/base/lang/buildfile/findusages/BuildReadWriteAccessDetector.java
index 8f01178..c28b741 100644
--- a/base/src/com/google/idea/blaze/base/lang/buildfile/findusages/BuildReadWriteAccessDetector.java
+++ b/base/src/com/google/idea/blaze/base/lang/buildfile/findusages/BuildReadWriteAccessDetector.java
@@ -46,7 +46,7 @@
       return Access.Write;
     }
     if (expression instanceof ReferenceExpression) {
-      if (PsiUtils.getParentOfType(expression, AugmentedAssignmentStatement.class) != null) {
+      if (PsiUtils.getParentOfType(expression, AugmentedAssignmentStatement.class, true) != null) {
         return Access.ReadWrite;
       }
     }
diff --git a/base/src/com/google/idea/blaze/base/lang/buildfile/findusages/BuildTargetElementEvaluator.java b/base/src/com/google/idea/blaze/base/lang/buildfile/findusages/BuildTargetElementEvaluator.java
index 7ef9399..a538b35 100644
--- a/base/src/com/google/idea/blaze/base/lang/buildfile/findusages/BuildTargetElementEvaluator.java
+++ b/base/src/com/google/idea/blaze/base/lang/buildfile/findusages/BuildTargetElementEvaluator.java
@@ -56,35 +56,32 @@
   }
 
   private static final Comparator<PsiReference> COMPARATOR =
-      new Comparator<PsiReference>() {
-        @Override
-        public int compare(final PsiReference ref1, final PsiReference ref2) {
-          boolean resolves1 = ref1.resolve() != null;
-          boolean resolves2 = ref2.resolve() != null;
-          if (resolves1 && !resolves2) {
-            return -1;
-          }
-          if (!resolves1 && resolves2) {
-            return 1;
-          }
+      (ref1, ref2) -> {
+        boolean resolves1 = ref1.resolve() != null;
+        boolean resolves2 = ref2.resolve() != null;
+        if (resolves1 && !resolves2) {
+          return -1;
+        }
+        if (!resolves1 && resolves2) {
+          return 1;
+        }
 
-          final TextRange range1 = ref1.getRangeInElement();
-          final TextRange range2 = ref2.getRangeInElement();
+        final TextRange range1 = ref1.getRangeInElement();
+        final TextRange range2 = ref2.getRangeInElement();
 
-          if (TextRange.areSegmentsEqual(range1, range2)) {
-            return 0;
-          }
-          if (range1.getStartOffset() >= range2.getStartOffset()
-              && range1.getEndOffset() <= range2.getEndOffset()) {
-            return 1;
-          }
-          if (range2.getStartOffset() >= range1.getStartOffset()
-              && range2.getEndOffset() <= range1.getEndOffset()) {
-            return -1;
-          }
-
+        if (TextRange.areSegmentsEqual(range1, range2)) {
           return 0;
         }
+        if (range1.getStartOffset() >= range2.getStartOffset()
+            && range1.getEndOffset() <= range2.getEndOffset()) {
+          return 1;
+        }
+        if (range2.getStartOffset() >= range1.getStartOffset()
+            && range2.getEndOffset() <= range1.getEndOffset()) {
+          return -1;
+        }
+
+        return 0;
       };
 
   /** Redirect 'name' funcall argument values to the funcall expression (b/29088829). */
diff --git a/base/src/com/google/idea/blaze/base/lang/buildfile/formatting/BuildCommenter.java b/base/src/com/google/idea/blaze/base/lang/buildfile/formatting/BuildCommenter.java
index 34af8b9..994107e 100644
--- a/base/src/com/google/idea/blaze/base/lang/buildfile/formatting/BuildCommenter.java
+++ b/base/src/com/google/idea/blaze/base/lang/buildfile/formatting/BuildCommenter.java
@@ -20,7 +20,7 @@
 import com.intellij.lang.CodeDocumentationAwareCommenter;
 import com.intellij.psi.PsiComment;
 import com.intellij.psi.tree.IElementType;
-import org.jetbrains.annotations.Nullable;
+import javax.annotation.Nullable;
 
 /** Supports (un)commenting lines via IntelliJ */
 public class BuildCommenter implements CodeDocumentationAwareCommenter {
diff --git a/base/src/com/google/idea/blaze/base/lang/buildfile/formatting/BuildLanguageCodeStyleSettingsProvider.java b/base/src/com/google/idea/blaze/base/lang/buildfile/formatting/BuildLanguageCodeStyleSettingsProvider.java
index b2295af..2562a55 100644
--- a/base/src/com/google/idea/blaze/base/lang/buildfile/formatting/BuildLanguageCodeStyleSettingsProvider.java
+++ b/base/src/com/google/idea/blaze/base/lang/buildfile/formatting/BuildLanguageCodeStyleSettingsProvider.java
@@ -21,8 +21,8 @@
 import com.intellij.lang.Language;
 import com.intellij.psi.codeStyle.CommonCodeStyleSettings;
 import com.intellij.psi.codeStyle.LanguageCodeStyleSettingsProvider;
+import javax.annotation.Nullable;
 import org.jetbrains.annotations.NotNull;
-import org.jetbrains.annotations.Nullable;
 
 /** Allows BUILD language-specific code style settings */
 public class BuildLanguageCodeStyleSettingsProvider extends LanguageCodeStyleSettingsProvider {
diff --git a/base/src/com/google/idea/blaze/base/lang/buildfile/globbing/UnixGlob.java b/base/src/com/google/idea/blaze/base/lang/buildfile/globbing/UnixGlob.java
index b7a44c4..ab7ede4 100644
--- a/base/src/com/google/idea/blaze/base/lang/buildfile/globbing/UnixGlob.java
+++ b/base/src/com/google/idea/blaze/base/lang/buildfile/globbing/UnixGlob.java
@@ -15,7 +15,6 @@
  */
 package com.google.idea.blaze.base.lang.buildfile.globbing;
 
-import com.google.common.base.Preconditions;
 import com.google.common.base.Splitter;
 import com.google.common.base.Throwables;
 import com.google.common.cache.Cache;
@@ -31,17 +30,14 @@
 import com.google.common.util.concurrent.SettableFuture;
 import com.google.idea.blaze.base.io.FileAttributeProvider;
 import com.google.idea.blaze.base.lang.buildfile.validation.GlobPatternValidator;
-import com.intellij.openapi.application.ApplicationManager;
 import com.intellij.openapi.progress.ProgressManager;
-import com.intellij.openapi.vfs.VirtualFile;
-import com.intellij.openapi.vfs.ex.temp.TempFileSystem;
 import java.io.File;
 import java.io.IOException;
 import java.util.ArrayList;
-import java.util.Arrays;
 import java.util.Collection;
 import java.util.Collections;
 import java.util.List;
+import java.util.Set;
 import java.util.concurrent.ExecutionException;
 import java.util.concurrent.Future;
 import java.util.concurrent.ThreadPoolExecutor;
@@ -49,6 +45,7 @@
 import java.util.concurrent.atomic.AtomicReference;
 import java.util.function.Predicate;
 import java.util.regex.Pattern;
+import java.util.regex.PatternSyntaxException;
 import javax.annotation.Nullable;
 
 /**
@@ -63,35 +60,16 @@
 public final class UnixGlob {
   private UnixGlob() {}
 
-  private static List<File> globInternal(
+  private static Set<File> globInternal(
       File base,
       Collection<String> patterns,
-      Collection<String> excludePatterns,
       boolean excludeDirectories,
       Predicate<File> dirPred,
       ThreadPoolExecutor threadPool)
       throws IOException, InterruptedException {
 
     GlobVisitor visitor = (threadPool == null) ? new GlobVisitor() : new GlobVisitor(threadPool);
-    return visitor.glob(base, patterns, excludePatterns, excludeDirectories, dirPred);
-  }
-
-  private static Future<List<File>> globAsyncInternal(
-      File base,
-      Collection<String> patterns,
-      Collection<String> excludePatterns,
-      boolean excludeDirectories,
-      Predicate<File> dirPred,
-      ThreadPoolExecutor threadPool) {
-    Preconditions.checkNotNull(threadPool, "%s %s", base, patterns);
-    try {
-      return new GlobVisitor(threadPool)
-          .globAsync(base, patterns, excludePatterns, excludeDirectories, dirPred);
-    } catch (IOException e) {
-      // We are evaluating asynchronously, so no exceptions should be thrown until the future is
-      // retrieved.
-      throw new IllegalStateException(e);
-    }
+    return visitor.glob(base, patterns, excludeDirectories, dirPred);
   }
 
   /**
@@ -113,58 +91,13 @@
     return list;
   }
 
-  private static boolean excludedOnMatch(
-      File path, List<String[]> excludePatterns, int idx, Cache<String, Pattern> cache) {
-    for (String[] excludePattern : excludePatterns) {
-      String text = path.getName();
-      if (idx == excludePattern.length && matches(excludePattern[idx - 1], text, cache)) {
-        return true;
-      }
-    }
-    return false;
-  }
-
-  /**
-   * Returns the exclude patterns in {@code excludePatterns} which could apply to the children of
-   * {@code base}
-   *
-   * @param idx index into {@code excludePatterns} for the part of the pattern which might match
-   *     {@code base}
-   */
-  private static List<String[]> getRelevantExcludes(
-      final File base,
-      List<String[]> excludePatterns,
-      final int idx,
-      final Cache<String, Pattern> cache) {
-
-    if (excludePatterns.isEmpty()) {
-      return excludePatterns;
-    }
-    List<String[]> list = new ArrayList<>();
-    for (String[] patterns : excludePatterns) {
-      if (excludePatternMatches(patterns, idx, base, cache)) {
-        list.add(patterns);
-      }
-    }
-    return list;
-  }
-
-  /**
-   * @param patterns a list of patterns
-   * @param idx index into {@code patterns}
-   */
-  private static boolean excludePatternMatches(
-      String[] patterns, int idx, File base, Cache<String, Pattern> cache) {
-    if (idx == 0) {
-      return true;
-    }
-    String text = base.getName();
-    return patterns.length > idx && matches(patterns[idx - 1], text, cache);
-  }
-
   /** Calls {@link #matches(String, String, Cache) matches(pattern, str, null)} */
   public static boolean matches(String pattern, String str) {
-    return matches(pattern, str, null);
+    try {
+      return matches(pattern, str, null);
+    } catch (PatternSyntaxException e) {
+      return false;
+    }
   }
 
   /**
@@ -176,7 +109,7 @@
    * @param patternCache a cache from patterns to compiled Pattern objects, or {@code null} to skip
    *     caching
    */
-  public static boolean matches(String pattern, String str, Cache<String, Pattern> patternCache) {
+  private static boolean matches(String pattern, String str, Cache<String, Pattern> patternCache) {
     if (pattern.length() == 0 || str.length() == 0) {
       return false;
     }
@@ -278,8 +211,8 @@
   /** Builder class for UnixGlob. */
   public static class Builder {
     private File base;
-    private List<String> patterns;
-    private List<String> excludes;
+    private final List<String> patterns = new ArrayList<>();
+    private final List<String> excludes = new ArrayList<>();
     private boolean excludeDirectories;
     private Predicate<File> pathFilter;
     private ThreadPoolExecutor threadPool;
@@ -287,8 +220,6 @@
     /** Creates a glob builder with the given base path. */
     public Builder(File base) {
       this.base = base;
-      this.patterns = Lists.newArrayList();
-      this.excludes = Lists.newArrayList();
       this.excludeDirectories = false;
       this.pathFilter = file -> true;
     }
@@ -374,37 +305,29 @@
      * @throws InterruptedException if the thread is interrupted.
      */
     public List<File> glob() throws IOException, InterruptedException {
-      return globInternal(base, patterns, excludes, excludeDirectories, pathFilter, threadPool);
-    }
-
-    /**
-     * Executes the glob asynchronously. {@link #setThreadPool} must have been called already with a
-     * non-null argument.
-     *
-     * @param checkForInterrupt if the returned future may throw InterruptedException.
-     */
-    public Future<List<File>> globAsync() {
-      return globAsyncInternal(
-          base, patterns, excludes, excludeDirectories, pathFilter, threadPool);
+      Set<File> included = globInternal(base, patterns, excludeDirectories, pathFilter, threadPool);
+      Set<File> excluded = globInternal(base, excludes, excludeDirectories, pathFilter, threadPool);
+      included.removeAll(excluded);
+      return Ordering.<File>natural().immutableSortedCopy(included);
     }
   }
 
   /** Adapts the result of the glob visitation as a Future. */
-  private static class GlobFuture extends ForwardingListenableFuture<List<File>> {
+  private static class GlobFuture extends ForwardingListenableFuture<Set<File>> {
     private final GlobVisitor visitor;
-    private final SettableFuture<List<File>> delegate = SettableFuture.create();
+    private final SettableFuture<Set<File>> delegate = SettableFuture.create();
 
-    public GlobFuture(GlobVisitor visitor) {
+    private GlobFuture(GlobVisitor visitor) {
       this.visitor = visitor;
     }
 
     @Override
-    public List<File> get() throws InterruptedException, ExecutionException {
+    public Set<File> get() throws InterruptedException, ExecutionException {
       return super.get();
     }
 
     @Override
-    protected ListenableFuture<List<File>> delegate() {
+    protected ListenableFuture<Set<File>> delegate() {
       return delegate;
     }
 
@@ -412,7 +335,7 @@
       delegate.setException(exception);
     }
 
-    public void set(List<File> paths) {
+    public void set(Set<File> paths) {
       delegate.set(paths);
     }
 
@@ -423,7 +346,7 @@
       return true;
     }
 
-    public void markCanceled() {
+    void markCanceled() {
       super.cancel(true);
     }
   }
@@ -434,7 +357,7 @@
    */
   private static final class GlobVisitor {
     // These collections are used across workers and must therefore be thread-safe.
-    private final Collection<File> results = Sets.newConcurrentHashSet();
+    private final Set<File> results = Sets.newConcurrentHashSet();
     private final Cache<String, Pattern> cache =
         CacheBuilder.newBuilder()
             .build(
@@ -452,40 +375,34 @@
     private final FileAttributeProvider fileAttributeProvider = FileAttributeProvider.getInstance();
     private volatile boolean canceled = false;
 
-    public GlobVisitor(ThreadPoolExecutor executor) {
+    private GlobVisitor(ThreadPoolExecutor executor) {
       this.executor = executor;
       this.result = new GlobFuture(this);
     }
 
-    public GlobVisitor() {
+    private GlobVisitor() {
       this(null);
     }
 
     /**
      * Performs wildcard globbing: returns the sorted list of filenames that match any of {@code
-     * patterns} relative to {@code base}, but which do not match {@code excludePatterns}.
-     * Directories are traversed if and only if they match {@code dirPred}. The predicate is also
-     * called for the root of the traversal.
+     * patterns} relative to {@code base}. Directories are traversed if and only if they match
+     * {@code dirPred}. The predicate is also called for the root of the traversal.
      *
      * <p>Patterns may include "*" and "?", but not "[a-z]".
      *
      * <p><code>**</code> gets special treatment in include patterns. If it is used as a complete
      * path segment it matches the filenames in subdirectories recursively.
      *
-     * @throws IllegalArgumentException if any glob or exclude pattern {@linkplain
-     *     #checkPatternForError(String) contains errors} or if any exclude pattern segment contains
-     *     <code>**</code> or if any include pattern segment contains <code>**</code> but not equal
-     *     to it.
+     * @throws IllegalArgumentException if any glob or exclude pattern contains errors, or if any
+     *     exclude pattern segment contains <code>**</code> or if any include pattern segment
+     *     contains <code>**</code> but not equal to it.
      */
-    public List<File> glob(
-        File base,
-        Collection<String> patterns,
-        Collection<String> excludePatterns,
-        boolean excludeDirectories,
-        Predicate<File> dirPred)
+    private Set<File> glob(
+        File base, Collection<String> patterns, boolean excludeDirectories, Predicate<File> dirPred)
         throws IOException, InterruptedException {
       try {
-        return globAsync(base, patterns, excludePatterns, excludeDirectories, dirPred).get();
+        return globAsync(base, patterns, excludeDirectories, dirPred).get();
       } catch (ExecutionException e) {
         Throwable cause = e.getCause();
         Throwables.propagateIfPossible(cause, IOException.class);
@@ -493,40 +410,24 @@
       }
     }
 
-    public Future<List<File>> globAsync(
-        File base,
-        Collection<String> patterns,
-        Collection<String> excludePatterns,
-        boolean excludeDirectories,
-        Predicate<File> dirPred)
+    private Future<Set<File>> globAsync(
+        File base, Collection<String> patterns, boolean excludeDirectories, Predicate<File> dirPred)
         throws IOException {
 
       if (!fileAttributeProvider.exists(base) || patterns.isEmpty()) {
-        return Futures.immediateFuture(Collections.emptyList());
+        return Futures.immediateFuture(Collections.emptySet());
       }
       boolean baseIsDirectory = fileAttributeProvider.isDirectory(base);
 
-      List<String[]> splitPatterns = checkAndSplitPatterns(patterns);
-      List<String[]> splitExcludes = checkAndSplitPatterns(excludePatterns);
-
       // We do a dumb loop, even though it will likely duplicate work
       // (e.g., readdir calls). In order to optimize, we would need
       // to keep track of which patterns shared sub-patterns and which did not
       // (for example consider the glob [*/*.java, sub/*.java, */*.txt]).
       pendingOps.incrementAndGet();
       try {
-        for (String[] splitPattern : splitPatterns) {
+        for (String[] splitPattern : checkAndSplitPatterns(patterns)) {
           queueGlob(
-              base,
-              baseIsDirectory,
-              splitPattern,
-              0,
-              excludeDirectories,
-              splitExcludes,
-              0,
-              results,
-              cache,
-              dirPred);
+              base, baseIsDirectory, splitPattern, 0, excludeDirectories, results, cache, dirPred);
         }
       } finally {
         decrementAndCheckDone();
@@ -541,8 +442,6 @@
         String[] patternParts,
         int idx,
         boolean excludeDirectories,
-        List<String[]> excludePatterns,
-        int excludeIdx,
         Collection<File> results,
         Cache<String, Pattern> cache,
         Predicate<File> dirPred)
@@ -556,8 +455,6 @@
                   patternParts,
                   idx,
                   excludeDirectories,
-                  excludePatterns,
-                  excludeIdx,
                   results,
                   cache,
                   dirPred);
@@ -567,7 +464,7 @@
           });
     }
 
-    protected void enqueue(final Runnable r) {
+    void enqueue(final Runnable r) {
       pendingOps.incrementAndGet();
 
       Runnable wrapped =
@@ -602,7 +499,7 @@
         } else if (failure.get() != null) {
           result.setException(failure.get());
         } else {
-          result.set(Ordering.<File>natural().immutableSortedCopy(results));
+          result.set(results);
         }
       }
     }
@@ -621,8 +518,6 @@
         String[] patternParts,
         int idx,
         boolean excludeDirectories,
-        List<String[]> excludePatterns,
-        int excludeIdx,
         Collection<File> results,
         Cache<String, Pattern> cache,
         Predicate<File> dirPred)
@@ -633,8 +528,7 @@
       }
 
       if (idx == patternParts.length) { // Base case.
-        if (!(excludeDirectories && baseIsDirectory)
-            && !excludedOnMatch(base, excludePatterns, excludeIdx, cache)) {
+        if (!(excludeDirectories && baseIsDirectory)) {
           results.add(base);
         }
         return;
@@ -645,8 +539,6 @@
         return;
       }
 
-      List<String[]> relevantExcludes =
-          getRelevantExcludes(base, excludePatterns, excludeIdx, cache);
       final String pattern = patternParts[idx];
 
       // ** is special: it can match nothing at all.
@@ -658,8 +550,6 @@
             patternParts,
             idx + 1,
             excludeDirectories,
-            excludePatterns,
-            excludeIdx,
             results,
             cache,
             dirPred);
@@ -675,16 +565,7 @@
         }
 
         queueGlob(
-            child,
-            childIsDir,
-            patternParts,
-            idx + 1,
-            excludeDirectories,
-            relevantExcludes,
-            excludeIdx + 1,
-            results,
-            cache,
-            dirPred);
+            child, childIsDir, patternParts, idx + 1, excludeDirectories, results, cache, dirPred);
         return;
       }
 
@@ -699,16 +580,7 @@
           // Recurse without shifting the pattern.
           if (childIsDir) {
             queueGlob(
-                child,
-                childIsDir,
-                patternParts,
-                idx,
-                excludeDirectories,
-                relevantExcludes,
-                excludeIdx + 1,
-                results,
-                cache,
-                dirPred);
+                child, childIsDir, patternParts, idx, excludeDirectories, results, cache, dirPred);
           }
         }
         if (matches(pattern, child.getName(), cache)) {
@@ -720,15 +592,12 @@
                 patternParts,
                 idx + 1,
                 excludeDirectories,
-                relevantExcludes,
-                excludeIdx + 1,
                 results,
                 cache,
                 dirPred);
           } else {
             // Instead of using an async call, just repeat the base case above.
-            if (idx + 1 == patternParts.length
-                && !excludedOnMatch(child, relevantExcludes, excludeIdx + 1, cache)) {
+            if (idx + 1 == patternParts.length) {
               results.add(child);
             }
           }
@@ -738,16 +607,7 @@
 
     @Nullable
     private File[] getChildren(File file) {
-      if (!ApplicationManager.getApplication().isUnitTestMode()) {
-        return file.listFiles();
-      }
-      TempFileSystem fs = TempFileSystem.getInstance();
-      VirtualFile vf = fs.findFileByIoFile(file);
-      VirtualFile[] children = vf.getChildren();
-      if (children == null) {
-        return null;
-      }
-      return Arrays.stream(children).map(VirtualFile::getPath).map(File::new).toArray(File[]::new);
+      return FileAttributeProvider.getInstance().listFiles(file);
     }
   }
 }
diff --git a/base/src/com/google/idea/blaze/base/lang/buildfile/language/BuildFileTypeFactory.java b/base/src/com/google/idea/blaze/base/lang/buildfile/language/BuildFileTypeFactory.java
index 4c4a21c..a73c0a4 100644
--- a/base/src/com/google/idea/blaze/base/lang/buildfile/language/BuildFileTypeFactory.java
+++ b/base/src/com/google/idea/blaze/base/lang/buildfile/language/BuildFileTypeFactory.java
@@ -15,8 +15,9 @@
  */
 package com.google.idea.blaze.base.lang.buildfile.language;
 
+import com.google.common.collect.ImmutableList;
 import com.google.idea.blaze.base.bazel.BuildSystemProvider;
-import com.intellij.openapi.fileTypes.ExtensionFileNameMatcher;
+import com.intellij.openapi.fileTypes.FileNameMatcher;
 import com.intellij.openapi.fileTypes.FileTypeConsumer;
 import com.intellij.openapi.fileTypes.FileTypeFactory;
 import org.jetbrains.annotations.NotNull;
@@ -26,9 +27,11 @@
 
   @Override
   public void createFileTypes(@NotNull final FileTypeConsumer consumer) {
-    consumer.consume(
-        BuildFileType.INSTANCE,
-        BuildSystemProvider.defaultBuildSystem().buildFileMatcher(),
-        new ExtensionFileNameMatcher("bzl"));
+    ImmutableList<FileNameMatcher> fileNameMatchers =
+        ImmutableList.<FileNameMatcher>builder()
+            .addAll(BuildSystemProvider.defaultBuildSystem().buildLanguageFileTypeMatchers())
+            .add()
+            .build();
+    consumer.consume(BuildFileType.INSTANCE, fileNameMatchers.toArray(new FileNameMatcher[0]));
   }
 }
diff --git a/base/src/com/google/idea/blaze/base/lang/buildfile/psi/Argument.java b/base/src/com/google/idea/blaze/base/lang/buildfile/psi/Argument.java
index 67ff005..ecf79f8 100644
--- a/base/src/com/google/idea/blaze/base/lang/buildfile/psi/Argument.java
+++ b/base/src/com/google/idea/blaze/base/lang/buildfile/psi/Argument.java
@@ -107,13 +107,15 @@
     }
   }
 
+  /** Variable number of positional arguments: *args */
   static class Star extends Argument {
     public Star(ASTNode node) {
       super(node);
     }
   }
 
-  static class StarStar extends Argument {
+  /** Variable number of keyword arguments: **kwargs */
+  public static class StarStar extends Argument {
     public StarStar(ASTNode node) {
       super(node);
     }
diff --git a/base/src/com/google/idea/blaze/base/lang/buildfile/psi/AssignmentStatement.java b/base/src/com/google/idea/blaze/base/lang/buildfile/psi/AssignmentStatement.java
index fc0d2e1..b1e34eb 100644
--- a/base/src/com/google/idea/blaze/base/lang/buildfile/psi/AssignmentStatement.java
+++ b/base/src/com/google/idea/blaze/base/lang/buildfile/psi/AssignmentStatement.java
@@ -16,6 +16,7 @@
 package com.google.idea.blaze.base.lang.buildfile.psi;
 
 import com.intellij.lang.ASTNode;
+import com.intellij.psi.PsiElement;
 import com.intellij.util.PlatformIcons;
 import javax.annotation.Nullable;
 import javax.swing.Icon;
@@ -36,7 +37,8 @@
   /** Returns the RHS of the assignment */
   @Nullable
   public Expression getAssignedValue() {
-    return childToPsi(BuildElementTypes.EXPRESSIONS, 1);
+    PsiElement psi = childToPsi(BuildElementTypes.EXPRESSIONS, 1);
+    return psi instanceof Expression ? (Expression) psi : null;
   }
 
   @Override
diff --git a/base/src/com/google/idea/blaze/base/lang/buildfile/psi/AugmentedAssignmentStatement.java b/base/src/com/google/idea/blaze/base/lang/buildfile/psi/AugmentedAssignmentStatement.java
index 5d2384d..c3a3eb0 100644
--- a/base/src/com/google/idea/blaze/base/lang/buildfile/psi/AugmentedAssignmentStatement.java
+++ b/base/src/com/google/idea/blaze/base/lang/buildfile/psi/AugmentedAssignmentStatement.java
@@ -16,6 +16,7 @@
 package com.google.idea.blaze.base.lang.buildfile.psi;
 
 import com.intellij.lang.ASTNode;
+import com.intellij.psi.PsiElement;
 import javax.annotation.Nullable;
 
 /** PSI element for an augmented assignment statement [expr += expr] */
@@ -28,13 +29,15 @@
   /** Returns the LHS of the assignment */
   @Nullable
   public TargetExpression getLeftHandSideExpression() {
-    return childToPsi(BuildElementTypes.EXPRESSIONS, 0);
+    PsiElement psi = childToPsi(BuildElementTypes.EXPRESSIONS, 0);
+    return psi instanceof TargetExpression ? (TargetExpression) psi : null;
   }
 
   /** Returns the RHS of the assignment */
   @Nullable
   public Expression getAssignedValue() {
-    return childToPsi(BuildElementTypes.EXPRESSIONS, 1);
+    PsiElement psi = childToPsi(BuildElementTypes.EXPRESSIONS, 1);
+    return psi instanceof Expression ? (Expression) psi : null;
   }
 
   @Override
diff --git a/base/src/com/google/idea/blaze/base/lang/buildfile/psi/BinaryOpExpression.java b/base/src/com/google/idea/blaze/base/lang/buildfile/psi/BinaryOpExpression.java
index 76a4387..3dab6d8 100644
--- a/base/src/com/google/idea/blaze/base/lang/buildfile/psi/BinaryOpExpression.java
+++ b/base/src/com/google/idea/blaze/base/lang/buildfile/psi/BinaryOpExpression.java
@@ -16,6 +16,7 @@
 package com.google.idea.blaze.base.lang.buildfile.psi;
 
 import com.intellij.lang.ASTNode;
+import com.intellij.psi.PsiElement;
 import javax.annotation.Nullable;
 
 /** PSI element for an binary operation expression [expr BIN_OP expr] */
@@ -33,12 +34,14 @@
   /** Returns the LHS of the expression */
   @Nullable
   public Expression getLhs() {
-    return childToPsi(BuildElementTypes.EXPRESSIONS, 0);
+    PsiElement psi = childToPsi(BuildElementTypes.EXPRESSIONS, 0);
+    return psi instanceof Expression ? (Expression) psi : null;
   }
 
   /** Returns the RHS of the expression */
   @Nullable
   public Expression getRhs() {
-    return childToPsi(BuildElementTypes.EXPRESSIONS, 1);
+    PsiElement psi = childToPsi(BuildElementTypes.EXPRESSIONS, 1);
+    return psi instanceof Expression ? (Expression) psi : null;
   }
 }
diff --git a/base/src/com/google/idea/blaze/base/lang/buildfile/psi/BuildElement.java b/base/src/com/google/idea/blaze/base/lang/buildfile/psi/BuildElement.java
index 35a67c5..7cfd64b 100644
--- a/base/src/com/google/idea/blaze/base/lang/buildfile/psi/BuildElement.java
+++ b/base/src/com/google/idea/blaze/base/lang/buildfile/psi/BuildElement.java
@@ -16,7 +16,6 @@
 package com.google.idea.blaze.base.lang.buildfile.psi;
 
 import com.google.idea.blaze.base.lang.buildfile.search.BlazePackage;
-import com.google.idea.blaze.base.model.primitives.WorkspacePath;
 import com.intellij.psi.NavigatablePsiElement;
 import com.intellij.psi.PsiElement;
 import javax.annotation.Nullable;
@@ -41,8 +40,6 @@
   @Nullable
   <P extends PsiElement> P firstChildOfClass(Class<P> psiClass);
 
-  WorkspacePath getWorkspacePath();
-
   @Nullable
   BlazePackage getBlazePackage();
 }
diff --git a/base/src/com/google/idea/blaze/base/lang/buildfile/psi/BuildElementImpl.java b/base/src/com/google/idea/blaze/base/lang/buildfile/psi/BuildElementImpl.java
index 55f472d..700f4dc 100644
--- a/base/src/com/google/idea/blaze/base/lang/buildfile/psi/BuildElementImpl.java
+++ b/base/src/com/google/idea/blaze/base/lang/buildfile/psi/BuildElementImpl.java
@@ -16,7 +16,6 @@
 package com.google.idea.blaze.base.lang.buildfile.psi;
 
 import com.google.idea.blaze.base.lang.buildfile.search.BlazePackage;
-import com.google.idea.blaze.base.model.primitives.WorkspacePath;
 import com.intellij.extapi.psi.ASTWrapperPsiElement;
 import com.intellij.lang.ASTNode;
 import com.intellij.navigation.ItemPresentation;
@@ -82,13 +81,14 @@
     return psiElements;
   }
 
+  /** Finds the n'th child of the specified type */
   @Nullable
-  protected <T extends BuildElement> T childToPsi(TokenSet filterSet, int index) {
+  protected PsiElement childToPsi(TokenSet filterSet, int index) {
     final ASTNode[] nodes = getNode().getChildren(filterSet);
     if (nodes.length <= index) {
       return null;
     }
-    return (T) nodes[index].getPsi();
+    return nodes[index].getPsi();
   }
 
   @Nullable
@@ -159,13 +159,6 @@
 
   @Nullable
   @Override
-  public WorkspacePath getWorkspacePath() {
-    BuildFile file = (BuildFile) getContainingFile();
-    return file.getWorkspacePath();
-  }
-
-  @Nullable
-  @Override
   public BlazePackage getBlazePackage() {
     PsiFile file = getContainingFile();
     return file != null ? BlazePackage.getContainingPackage(file) : null;
diff --git a/base/src/com/google/idea/blaze/base/lang/buildfile/psi/BuildFile.java b/base/src/com/google/idea/blaze/base/lang/buildfile/psi/BuildFile.java
index b9cc459..75c6417 100644
--- a/base/src/com/google/idea/blaze/base/lang/buildfile/psi/BuildFile.java
+++ b/base/src/com/google/idea/blaze/base/lang/buildfile/psi/BuildFile.java
@@ -15,26 +15,22 @@
  */
 package com.google.idea.blaze.base.lang.buildfile.psi;
 
-import com.google.common.collect.Lists;
 import com.google.idea.blaze.base.lang.buildfile.language.BuildFileType;
-import com.google.idea.blaze.base.lang.buildfile.references.BuildReferenceManager;
 import com.google.idea.blaze.base.lang.buildfile.references.QuoteType;
 import com.google.idea.blaze.base.lang.buildfile.search.BlazePackage;
-import com.google.idea.blaze.base.lang.buildfile.search.ResolveUtil;
-import com.google.idea.blaze.base.model.primitives.WorkspacePath;
+import com.google.idea.blaze.base.model.primitives.Label;
+import com.google.idea.blaze.base.sync.workspace.WorkspaceHelper;
 import com.intellij.extapi.psi.PsiFileBase;
-import com.intellij.lang.ASTNode;
+import com.intellij.navigation.ItemPresentation;
 import com.intellij.openapi.fileTypes.FileType;
 import com.intellij.openapi.project.Project;
 import com.intellij.psi.FileViewProvider;
 import com.intellij.psi.PsiElement;
 import com.intellij.psi.PsiNamedElement;
-import com.intellij.psi.tree.TokenSet;
 import com.intellij.util.PathUtil;
 import com.intellij.util.Processor;
 import icons.BlazeIcons;
 import java.io.File;
-import java.util.List;
 import javax.annotation.Nullable;
 import javax.swing.Icon;
 
@@ -44,24 +40,17 @@
   /** The blaze file type */
   public enum BlazeFileType {
     SkylarkExtension,
-    BuildPackage // "BUILD", plus hacks such as "BUILD.tools", "BUILD.bazel"
-  }
-
-  @Nullable
-  public static WorkspacePath getWorkspacePath(Project project, String filePath) {
-    return BuildReferenceManager.getInstance(project).getWorkspaceRelativePath(filePath);
+    BuildPackage, // "BUILD", "BUILD.bazel"
+    Workspace, // the top-level WORKSPACE file
   }
 
   public static String getBuildFileString(Project project, String filePath) {
-    WorkspacePath workspacePath = getWorkspacePath(project, PathUtil.getParentPath(filePath));
-    if (workspacePath == null) {
+    Label label = WorkspaceHelper.getBuildLabel(project, new File(filePath));
+    if (label == null) {
       return "BUILD file: " + filePath;
     }
-    String fileName = PathUtil.getFileName(filePath);
-    if (fileName.startsWith("BUILD")) {
-      return "//" + workspacePath + "/" + fileName;
-    }
-    return "//" + workspacePath + ":" + fileName;
+    String labelString = label.toString();
+    return labelString.replace(":__pkg__", "/" + PathUtil.getFileName(filePath));
   }
 
   public BuildFile(FileViewProvider viewProvider) {
@@ -78,6 +67,9 @@
     if (fileName.startsWith("BUILD")) {
       return BlazeFileType.BuildPackage;
     }
+    if (fileName.equals("WORKSPACE")) {
+      return BlazeFileType.Workspace;
+    }
     return BlazeFileType.SkylarkExtension;
   }
 
@@ -117,37 +109,19 @@
     return new File(getFilePath());
   }
 
-  @Nullable
-  @Override
-  public WorkspacePath getWorkspacePath() {
-    return getWorkspacePath(getProject(), getFilePath());
-  }
-
   /**
-   * The workspace path of the containing blaze package (this is always the parent directory for
-   * BUILD files, but may be a more distant ancestor for Skylark extensions)
+   * The label of the containing blaze package (this is always the parent directory for BUILD files,
+   * but may be a more distant ancestor for Skylark extensions)
    */
   @Nullable
-  public WorkspacePath getPackageWorkspacePath() {
+  public Label getPackageLabel() {
     BlazePackage parentPackage = getBlazePackage();
-    if (parentPackage == null) {
-      return null;
-    }
-    String filePath = parentPackage.buildFile.getFilePath();
-    return filePath != null
-        ? getWorkspacePath(getProject(), PathUtil.getParentPath(filePath))
-        : null;
-  }
-
-  @Nullable
-  public String getWorkspaceRelativePackagePath() {
-    WorkspacePath packagePath = getPackageWorkspacePath();
-    return packagePath != null ? packagePath.relativePath() : null;
+    return parentPackage != null ? parentPackage.getPackageLabel() : null;
   }
 
   /** The path for this file, formatted as a BUILD label. */
   @Nullable
-  public String getBuildLabel() {
+  public Label getBuildLabel() {
     BlazePackage containingPackage = getBlazePackage();
     return containingPackage != null
         ? containingPackage.getBuildLabelForChild(getFilePath())
@@ -166,24 +140,6 @@
     return null;
   }
 
-  /** .bzl files referenced in 'load' statements */
-  @Nullable
-  public String[] getImportedPaths() {
-    ASTNode[] loadStatements =
-        getNode().getChildren(TokenSet.create(BuildElementTypes.LOAD_STATEMENT));
-    if (loadStatements.length == 0) {
-      return null;
-    }
-    List<String> importedPaths = Lists.newArrayListWithCapacity(loadStatements.length);
-    for (int i = 0; i < loadStatements.length; i++) {
-      String path = ((LoadStatement) loadStatements[i].getPsi()).getImportedPath();
-      if (path != null) {
-        importedPaths.add(path);
-      }
-    }
-    return importedPaths.toArray(new String[importedPaths.size()]);
-  }
-
   @Nullable
   public FunctionStatement findDeclaredFunction(String name) {
     for (FunctionStatement fn : getFunctionDeclarations()) {
@@ -195,11 +151,6 @@
   }
 
   @Nullable
-  public TargetExpression findTopLevelVariable(String name) {
-    return ResolveUtil.searchChildAssignmentStatements(this, name);
-  }
-
-  @Nullable
   public FunctionStatement findLoadedFunction(String name) {
     for (LoadStatement loadStatement : findChildrenByClass(LoadStatement.class)) {
       for (LoadedSymbol loadedSymbol : loadStatement.getImportedSymbolElements()) {
@@ -293,6 +244,28 @@
   }
 
   @Override
+  public ItemPresentation getPresentation() {
+    final BuildFile element = this;
+    return new ItemPresentation() {
+      @Override
+      public String getPresentableText() {
+        return element.getName();
+      }
+
+      @Override
+      public String getLocationString() {
+        String label = getBuildFileString(element.getProject(), element.getFilePath());
+        return String.format("(%s)", label);
+      }
+
+      @Override
+      public Icon getIcon(boolean unused) {
+        return element.getIcon(0);
+      }
+    };
+  }
+
+  @Override
   public String toString() {
     return getBuildFileString(getProject(), getFilePath());
   }
diff --git a/base/src/com/google/idea/blaze/base/lang/buildfile/psi/FuncallExpression.java b/base/src/com/google/idea/blaze/base/lang/buildfile/psi/FuncallExpression.java
index e59d9a7..46701da 100644
--- a/base/src/com/google/idea/blaze/base/lang/buildfile/psi/FuncallExpression.java
+++ b/base/src/com/google/idea/blaze/base/lang/buildfile/psi/FuncallExpression.java
@@ -15,8 +15,7 @@
  */
 package com.google.idea.blaze.base.lang.buildfile.psi;
 
-import com.google.idea.blaze.base.lang.buildfile.language.semantics.BuildLanguageSpec;
-import com.google.idea.blaze.base.lang.buildfile.language.semantics.BuildLanguageSpecProvider;
+import com.google.idea.blaze.base.lang.buildfile.psi.BuildFile.BlazeFileType;
 import com.google.idea.blaze.base.lang.buildfile.psi.util.PsiUtils;
 import com.google.idea.blaze.base.lang.buildfile.references.FuncallReference;
 import com.google.idea.blaze.base.lang.buildfile.references.LabelUtils;
@@ -106,8 +105,10 @@
   @Nullable
   public Label resolveBuildLabel() {
     BuildFile containingFile = getContainingFile();
-    if (containingFile == null
-        || containingFile.getBlazeFileType() == BuildFile.BlazeFileType.SkylarkExtension) {
+    if (containingFile == null) {
+      return null;
+    }
+    if (containingFile.getBlazeFileType() != BlazeFileType.BuildPackage) {
       return null;
     }
     return LabelUtils.createLabelFromRuleName(getBlazePackage(), getNameArgumentValue());
@@ -175,11 +176,6 @@
     if (nameNode == null) {
       return null;
     }
-    BuildLanguageSpec spec = BuildLanguageSpecProvider.getInstance().getLanguageSpec(getProject());
-    if (spec != null && spec.hasRule(nameNode.getText())) {
-      // don't try to follow references to built-in rules
-      return null;
-    }
     TextRange range = PsiUtils.childRangeInParent(getTextRange(), nameNode.getTextRange());
     return new FuncallReference(this, range);
   }
diff --git a/base/src/com/google/idea/blaze/base/lang/buildfile/psi/FunctionStatement.java b/base/src/com/google/idea/blaze/base/lang/buildfile/psi/FunctionStatement.java
index 0dce2fe..3865d64 100644
--- a/base/src/com/google/idea/blaze/base/lang/buildfile/psi/FunctionStatement.java
+++ b/base/src/com/google/idea/blaze/base/lang/buildfile/psi/FunctionStatement.java
@@ -19,8 +19,8 @@
 import com.intellij.lang.ASTNode;
 import com.intellij.psi.PsiElement;
 import com.intellij.util.PlatformIcons;
+import javax.annotation.Nullable;
 import javax.swing.Icon;
-import org.jetbrains.annotations.Nullable;
 
 /** PSI element for a function definition statement. */
 public class FunctionStatement extends NamedBuildElement
diff --git a/base/src/com/google/idea/blaze/base/lang/buildfile/psi/Parameter.java b/base/src/com/google/idea/blaze/base/lang/buildfile/psi/Parameter.java
index c873bb6..37298d2 100644
--- a/base/src/com/google/idea/blaze/base/lang/buildfile/psi/Parameter.java
+++ b/base/src/com/google/idea/blaze/base/lang/buildfile/psi/Parameter.java
@@ -19,8 +19,8 @@
 import com.google.idea.blaze.base.lang.buildfile.lexer.TokenKind;
 import com.intellij.icons.AllIcons;
 import com.intellij.lang.ASTNode;
+import javax.annotation.Nullable;
 import javax.swing.Icon;
-import org.jetbrains.annotations.Nullable;
 
 /** PSI nodes for parameters in a function declaration */
 public abstract class Parameter extends NamedBuildElement {
diff --git a/base/src/com/google/idea/blaze/base/lang/buildfile/psi/ReferenceExpression.java b/base/src/com/google/idea/blaze/base/lang/buildfile/psi/ReferenceExpression.java
index 287402c..755231e 100644
--- a/base/src/com/google/idea/blaze/base/lang/buildfile/psi/ReferenceExpression.java
+++ b/base/src/com/google/idea/blaze/base/lang/buildfile/psi/ReferenceExpression.java
@@ -20,7 +20,7 @@
 import com.intellij.lang.ASTNode;
 import com.intellij.psi.PsiReference;
 import com.intellij.psi.tree.IElementType;
-import org.jetbrains.annotations.Nullable;
+import javax.annotation.Nullable;
 
 /** References a PsiNamedElement */
 public class ReferenceExpression extends BuildElementImpl implements Expression {
diff --git a/base/src/com/google/idea/blaze/base/lang/buildfile/psi/ReturnStatement.java b/base/src/com/google/idea/blaze/base/lang/buildfile/psi/ReturnStatement.java
index a334c00..63926ee 100644
--- a/base/src/com/google/idea/blaze/base/lang/buildfile/psi/ReturnStatement.java
+++ b/base/src/com/google/idea/blaze/base/lang/buildfile/psi/ReturnStatement.java
@@ -16,6 +16,7 @@
 package com.google.idea.blaze.base.lang.buildfile.psi;
 
 import com.intellij.lang.ASTNode;
+import com.intellij.psi.PsiElement;
 import javax.annotation.Nullable;
 
 /** A wrapper Statement class for return expressions. */
@@ -27,7 +28,8 @@
 
   @Nullable
   public Expression getReturnExpression() {
-    return childToPsi(BuildElementTypes.EXPRESSIONS, 0);
+    PsiElement psi = childToPsi(BuildElementTypes.EXPRESSIONS, 0);
+    return psi instanceof Expression ? (Expression) psi : null;
   }
 
   @Override
diff --git a/base/src/com/google/idea/blaze/base/lang/buildfile/psi/StringLiteral.java b/base/src/com/google/idea/blaze/base/lang/buildfile/psi/StringLiteral.java
index 027107e..933bd47 100644
--- a/base/src/com/google/idea/blaze/base/lang/buildfile/psi/StringLiteral.java
+++ b/base/src/com/google/idea/blaze/base/lang/buildfile/psi/StringLiteral.java
@@ -18,6 +18,7 @@
 import com.google.idea.blaze.base.lang.buildfile.psi.Argument.Keyword;
 import com.google.idea.blaze.base.lang.buildfile.psi.util.PsiUtils;
 import com.google.idea.blaze.base.lang.buildfile.references.AttributeSpecificStringLiteralReferenceProvider;
+import com.google.idea.blaze.base.lang.buildfile.references.ExternalWorkspaceReferenceFragment;
 import com.google.idea.blaze.base.lang.buildfile.references.LabelReference;
 import com.google.idea.blaze.base.lang.buildfile.references.LoadedSymbolReference;
 import com.google.idea.blaze.base.lang.buildfile.references.PackageReferenceFragment;
@@ -128,8 +129,11 @@
     }
     PsiReference primaryReference = getReference();
     if (primaryReference instanceof LabelReference) {
+      LabelReference labelReference = (LabelReference) primaryReference;
       return new PsiReference[] {
-        primaryReference, new PackageReferenceFragment((LabelReference) primaryReference)
+        primaryReference,
+        new PackageReferenceFragment(labelReference),
+        new ExternalWorkspaceReferenceFragment(labelReference)
       };
     }
     return primaryReference != null
@@ -159,7 +163,7 @@
   /** If this string is an attribute value within a BUILD rule, return the attribute type. */
   @Nullable
   private String getParentAttributeName() {
-    Keyword parentKeyword = PsiUtils.getParentOfType(this, Keyword.class);
+    Keyword parentKeyword = PsiUtils.getParentOfType(this, Keyword.class, true);
     return parentKeyword != null ? parentKeyword.getName() : null;
   }
 
diff --git a/base/src/com/google/idea/blaze/base/lang/buildfile/psi/TargetExpression.java b/base/src/com/google/idea/blaze/base/lang/buildfile/psi/TargetExpression.java
index 405d0de..96a87ec 100644
--- a/base/src/com/google/idea/blaze/base/lang/buildfile/psi/TargetExpression.java
+++ b/base/src/com/google/idea/blaze/base/lang/buildfile/psi/TargetExpression.java
@@ -19,8 +19,8 @@
 import com.intellij.lang.ASTNode;
 import com.intellij.psi.PsiReference;
 import com.intellij.util.PlatformIcons;
+import javax.annotation.Nullable;
 import javax.swing.Icon;
-import org.jetbrains.annotations.Nullable;
 
 /** References a PsiNamedElement */
 public class TargetExpression extends NamedBuildElement implements Expression {
diff --git a/base/src/com/google/idea/blaze/base/lang/buildfile/psi/util/PsiUtils.java b/base/src/com/google/idea/blaze/base/lang/buildfile/psi/util/PsiUtils.java
index 30d8c5e..213586b 100644
--- a/base/src/com/google/idea/blaze/base/lang/buildfile/psi/util/PsiUtils.java
+++ b/base/src/com/google/idea/blaze/base/lang/buildfile/psi/util/PsiUtils.java
@@ -67,13 +67,14 @@
    * a parent of type PsiDirectory.
    */
   @Nullable
-  public static <T extends PsiElement> T getParentOfType(PsiElement element, Class<T> psiClass) {
-    PsiElement parent = element.getParent();
-    while (parent != null && !(parent instanceof PsiDirectory)) {
-      if (psiClass.isInstance(parent)) {
-        return (T) parent;
+  public static <T extends PsiElement> T getParentOfType(
+      PsiElement element, Class<T> psiClass, boolean strict) {
+    element = strict ? element.getParent() : element;
+    while (element != null && !(element instanceof PsiDirectory)) {
+      if (psiClass.isInstance(element)) {
+        return psiClass.cast(element);
       }
-      parent = parent.getParent();
+      element = element.getParent();
     }
     return null;
   }
@@ -159,7 +160,7 @@
    * Unwraps ParenthesizedExpression.<br>
    * For other types, returns the input expression.
    */
-  public static PsiElement getReferencedTarget(Expression expr) {
+  private static PsiElement getReferencedTarget(Expression expr) {
     PsiElement element = expr;
     while (true) {
       PsiElement unwrapped = unwrap(element);
@@ -170,6 +171,7 @@
     }
   }
 
+  @Nullable
   private static PsiElement unwrap(PsiElement expr) {
     if (expr instanceof ParenthesizedExpression) {
       return ((ParenthesizedExpression) expr).getContainedExpression();
diff --git a/base/src/com/google/idea/blaze/base/lang/buildfile/quickfix/DeprecatedLoadQuickFix.java b/base/src/com/google/idea/blaze/base/lang/buildfile/quickfix/DeprecatedLoadQuickFix.java
new file mode 100644
index 0000000..2fdb3a3
--- /dev/null
+++ b/base/src/com/google/idea/blaze/base/lang/buildfile/quickfix/DeprecatedLoadQuickFix.java
@@ -0,0 +1,105 @@
+/*
+ * Copyright 2017 The Bazel Authors. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.idea.blaze.base.lang.buildfile.quickfix;
+
+import com.google.idea.blaze.base.bazel.BuildSystemProvider;
+import com.google.idea.blaze.base.lang.buildfile.psi.StringLiteral;
+import com.google.idea.blaze.base.lang.buildfile.psi.util.PsiUtils;
+import com.google.idea.blaze.base.lang.buildfile.references.LabelUtils;
+import com.google.idea.blaze.base.model.primitives.WorkspacePath;
+import com.google.idea.blaze.base.model.primitives.WorkspaceRoot;
+import com.google.idea.blaze.base.settings.Blaze;
+import com.intellij.codeInsight.intention.HighPriorityAction;
+import com.intellij.codeInspection.LocalQuickFix;
+import com.intellij.codeInspection.ProblemDescriptor;
+import com.intellij.history.core.Paths;
+import com.intellij.lang.ASTNode;
+import com.intellij.openapi.project.Project;
+import com.intellij.psi.PsiElement;
+import java.io.File;
+import javax.annotation.Nullable;
+
+/** Offer to convert deprecated statements to label format. */
+public class DeprecatedLoadQuickFix implements LocalQuickFix, HighPriorityAction {
+
+  public static final DeprecatedLoadQuickFix INSTANCE = new DeprecatedLoadQuickFix();
+
+  @Override
+  public String getFamilyName() {
+    return "Fix load statement format";
+  }
+
+  @Override
+  public String getName() {
+    return getFamilyName();
+  }
+
+  @Override
+  public void applyFix(Project project, ProblemDescriptor descriptor) {
+    PsiElement element = descriptor.getPsiElement();
+    if (element instanceof StringLiteral) {
+      fixLoadString(project, (StringLiteral) element);
+    }
+  }
+
+  private static void fixLoadString(Project project, StringLiteral importString) {
+    String contents = importString.getStringContents();
+    if (!contents.startsWith("/") || LabelUtils.isAbsolute(contents)) {
+      return;
+    }
+    WorkspaceRoot root = WorkspaceRoot.fromProjectSafe(project);
+    if (root == null) {
+      return;
+    }
+    WorkspacePath workspacePath = WorkspacePath.createIfValid(contents.substring(1));
+    if (workspacePath == null) {
+      return;
+    }
+    File file = root.fileForPath(workspacePath);
+    File parentPackageFile = findContainingPackage(project, file);
+    if (parentPackageFile == null) {
+      return;
+    }
+    WorkspacePath packagePath = root.workspacePathForSafe(parentPackageFile);
+    if (packagePath == null) {
+      return;
+    }
+    String relativePath =
+        Paths.relativeIfUnder(workspacePath.relativePath(), packagePath.relativePath());
+    if (relativePath == null) {
+      return;
+    }
+    String newString = "//" + packagePath + ":" + relativePath + ".bzl";
+
+    ASTNode node = importString.getNode();
+    node.replaceChild(
+        node.getFirstChildNode(), PsiUtils.createNewLabel(importString.getProject(), newString));
+  }
+
+  @Nullable
+  private static File findContainingPackage(Project project, File file) {
+    BuildSystemProvider provider = Blaze.getBuildSystemProvider(project);
+    file = file.getParentFile();
+    while (file != null) {
+      File buildFile = provider.findBuildFileInDirectory(file);
+      if (buildFile != null) {
+        return file;
+      }
+      file = file.getParentFile();
+    }
+    return null;
+  }
+}
diff --git a/base/src/com/google/idea/blaze/base/lang/buildfile/refactor/BuildNamesValidator.java b/base/src/com/google/idea/blaze/base/lang/buildfile/refactor/BuildNamesValidator.java
index f85425d..ded303f 100644
--- a/base/src/com/google/idea/blaze/base/lang/buildfile/refactor/BuildNamesValidator.java
+++ b/base/src/com/google/idea/blaze/base/lang/buildfile/refactor/BuildNamesValidator.java
@@ -15,18 +15,24 @@
  */
 package com.google.idea.blaze.base.lang.buildfile.refactor;
 
+import com.google.common.collect.ImmutableSet;
 import com.google.idea.blaze.base.lang.buildfile.lexer.BuildLexer;
 import com.google.idea.blaze.base.lang.buildfile.lexer.BuildLexerBase;
 import com.google.idea.blaze.base.lang.buildfile.lexer.TokenKind;
 import com.intellij.lang.refactoring.NamesValidator;
 import com.intellij.openapi.project.Project;
+import java.util.stream.Collectors;
 
-/** Used for rename validation */
+/** Used for rename validation of elements which aren't string literals. */
 public class BuildNamesValidator implements NamesValidator {
 
+  private static final ImmutableSet<String> KEYWORDS =
+      ImmutableSet.copyOf(
+          TokenKind.KEYWORDS.stream().map(TokenKind::toString).collect(Collectors.toSet()));
+
   @Override
   public boolean isKeyword(String s, Project project) {
-    return false;
+    return KEYWORDS.contains(s);
   }
 
   @Override
diff --git a/base/src/com/google/idea/blaze/base/lang/buildfile/refactor/TargetRenameValidator.java b/base/src/com/google/idea/blaze/base/lang/buildfile/refactor/TargetRenameValidator.java
new file mode 100644
index 0000000..a46ccad
--- /dev/null
+++ b/base/src/com/google/idea/blaze/base/lang/buildfile/refactor/TargetRenameValidator.java
@@ -0,0 +1,39 @@
+/*
+ * Copyright 2017 The Bazel Authors. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.idea.blaze.base.lang.buildfile.refactor;
+
+import com.google.idea.blaze.base.lang.buildfile.psi.FuncallExpression;
+import com.google.idea.blaze.base.model.primitives.TargetName;
+import com.intellij.patterns.ElementPattern;
+import com.intellij.patterns.PlatformPatterns;
+import com.intellij.psi.PsiElement;
+import com.intellij.refactoring.rename.RenameInputValidator;
+import com.intellij.util.ProcessingContext;
+
+/** Overrides name validation for target names. */
+public class TargetRenameValidator implements RenameInputValidator {
+
+  @Override
+  public ElementPattern<? extends PsiElement> getPattern() {
+    // FuncallExpression is the owner of the target name.
+    return PlatformPatterns.psiElement(FuncallExpression.class);
+  }
+
+  @Override
+  public boolean isInputValid(String newName, PsiElement element, ProcessingContext context) {
+    return TargetName.validate(newName);
+  }
+}
diff --git a/base/src/com/google/idea/blaze/base/lang/buildfile/references/BuildReferenceManager.java b/base/src/com/google/idea/blaze/base/lang/buildfile/references/BuildReferenceManager.java
index 9b72b1d..4738bdd 100644
--- a/base/src/com/google/idea/blaze/base/lang/buildfile/references/BuildReferenceManager.java
+++ b/base/src/com/google/idea/blaze/base/lang/buildfile/references/BuildReferenceManager.java
@@ -25,6 +25,7 @@
 import com.google.idea.blaze.base.model.primitives.TargetName;
 import com.google.idea.blaze.base.model.primitives.WorkspacePath;
 import com.google.idea.blaze.base.settings.Blaze;
+import com.google.idea.blaze.base.sync.workspace.WorkspaceHelper;
 import com.google.idea.blaze.base.sync.workspace.WorkspacePathResolver;
 import com.google.idea.blaze.base.sync.workspace.WorkspacePathResolverProvider;
 import com.intellij.openapi.components.ServiceManager;
@@ -58,19 +59,19 @@
   /** Finds the PSI element associated with the given label. */
   @Nullable
   public PsiElement resolveLabel(Label label) {
-    return resolveLabel(label.blazePackage(), label.targetName(), false);
+    return resolveLabel(label, false);
   }
 
   /** Finds the PSI element associated with the given label. */
   @Nullable
-  public PsiElement resolveLabel(
-      WorkspacePath packagePath, TargetName targetName, boolean excludeRules) {
-    File packageDir = resolvePackage(packagePath);
+  public PsiElement resolveLabel(Label label, boolean excludeRules) {
+    File packageDir = WorkspaceHelper.resolveBlazePackage(project, label);
     if (packageDir == null) {
       return null;
     }
 
-    if (targetName.toString().equals("__pkg__")) {
+    String targetName = label.targetName().toString();
+    if (targetName.equals("__pkg__")) {
       return findBuildFile(packageDir);
     }
 
@@ -82,7 +83,7 @@
     }
 
     // try a direct file reference (e.g. ":a.java")
-    File fullFile = new File(packageDir, targetName.toString());
+    File fullFile = new File(packageDir, targetName);
     if (FileAttributeProvider.getInstance().exists(fullFile)) {
       return resolveFile(fullFile);
     }
@@ -90,9 +91,9 @@
     return null;
   }
 
-  private FuncallExpression findRule(File packageDir, TargetName targetName) {
+  private FuncallExpression findRule(File packageDir, String targetName) {
     BuildFile psiFile = findBuildFile(packageDir);
-    return psiFile != null ? psiFile.findRule(targetName.toString()) : null;
+    return psiFile != null ? psiFile.findRule(targetName) : null;
   }
 
   @Nullable
@@ -190,7 +191,8 @@
   }
 
   private BuildLookupElement lookupForFile(VirtualFile file, FileLookupData lookupData) {
-    WorkspacePath workspacePath = getWorkspaceRelativePath(file.getPath());
+    WorkspacePath workspacePath =
+        WorkspaceHelper.resolveWorkspacePath(project, new File(file.getPath()));
     return lookupData.lookupElementForFile(project, file, workspacePath);
   }
 
@@ -244,10 +246,4 @@
     String rulePathParent = PathUtil.getParentPath(targetName.toString());
     return new File(packageFile, rulePathParent);
   }
-
-  @Nullable
-  public WorkspacePath getWorkspaceRelativePath(String absolutePath) {
-    WorkspacePathResolver pathResolver = getWorkspacePathResolver();
-    return pathResolver != null ? pathResolver.getWorkspacePath(new File(absolutePath)) : null;
-  }
 }
diff --git a/base/src/com/google/idea/blaze/base/lang/buildfile/references/ExternalWorkspaceReferenceFragment.java b/base/src/com/google/idea/blaze/base/lang/buildfile/references/ExternalWorkspaceReferenceFragment.java
new file mode 100644
index 0000000..521f432
--- /dev/null
+++ b/base/src/com/google/idea/blaze/base/lang/buildfile/references/ExternalWorkspaceReferenceFragment.java
@@ -0,0 +1,105 @@
+/*
+ * Copyright 2016 The Bazel Authors. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.idea.blaze.base.lang.buildfile.references;
+
+import com.google.idea.blaze.base.lang.buildfile.psi.BuildFile;
+import com.google.idea.blaze.base.lang.buildfile.psi.FuncallExpression;
+import com.google.idea.blaze.base.lang.buildfile.psi.StringLiteral;
+import com.google.idea.blaze.base.lang.buildfile.psi.util.PsiUtils;
+import com.google.idea.blaze.base.model.primitives.WorkspacePath;
+import com.google.idea.blaze.base.model.primitives.WorkspaceRoot;
+import com.intellij.lang.ASTNode;
+import com.intellij.openapi.project.Project;
+import com.intellij.openapi.util.TextRange;
+import com.intellij.psi.PsiElement;
+import com.intellij.psi.PsiFileSystemItem;
+import com.intellij.psi.PsiReferenceBase;
+import com.intellij.util.IncorrectOperationException;
+import com.intellij.util.ObjectUtils;
+import javax.annotation.Nullable;
+
+/** The external workspace component of a label (between '@' and '//') */
+public class ExternalWorkspaceReferenceFragment extends PsiReferenceBase<StringLiteral> {
+
+  public ExternalWorkspaceReferenceFragment(LabelReference labelReference) {
+    super(labelReference.getElement(), labelReference.getRangeInElement(), labelReference.isSoft());
+  }
+
+  @Override
+  public TextRange getRangeInElement() {
+    String rawText = myElement.getText();
+    boolean valid = LabelUtils.getExternalWorkspaceComponent(myElement.getStringContents()) != null;
+    if (!valid) {
+      return TextRange.EMPTY_RANGE;
+    }
+    int endIndex = rawText.indexOf("//");
+    if (endIndex == -1) {
+      endIndex = rawText.length() - 1;
+    }
+    return new TextRange(1, endIndex);
+  }
+
+  @Nullable
+  @Override
+  public FuncallExpression resolve() {
+    String name = LabelUtils.getExternalWorkspaceComponent(myElement.getStringContents());
+    if (name == null) {
+      return null;
+    }
+    BuildFile workspaceFile = resolveProjectWorkspaceFile(myElement.getProject());
+    return workspaceFile != null ? workspaceFile.findRule(name) : null;
+  }
+
+  @Nullable
+  private static BuildFile resolveProjectWorkspaceFile(Project project) {
+    WorkspaceRoot projectRoot = WorkspaceRoot.fromProject(project);
+    if (projectRoot == null) {
+      return null;
+    }
+    PsiFileSystemItem workspaceFile =
+        BuildReferenceManager.getInstance(project)
+            .resolveFile(projectRoot.fileForPath(new WorkspacePath("WORKSPACE")));
+    return ObjectUtils.tryCast(workspaceFile, BuildFile.class);
+  }
+
+  @Override
+  public Object[] getVariants() {
+    return EMPTY_ARRAY;
+  }
+
+  @Override
+  public PsiElement handleElementRename(String newElementName) throws IncorrectOperationException {
+    String oldString = myElement.getStringContents();
+    int slashesIndex = oldString.indexOf("//");
+    String newString =
+        String.format(
+            "@%s%s", newElementName, slashesIndex == -1 ? "" : oldString.substring(slashesIndex));
+    return handleRename(newString);
+  }
+
+  @Override
+  public PsiElement bindToElement(PsiElement element) throws IncorrectOperationException {
+    return myElement;
+  }
+
+  private PsiElement handleRename(String newStringContents) {
+    ASTNode node = myElement.getNode();
+    node.replaceChild(
+        node.getFirstChildNode(),
+        PsiUtils.createNewLabel(myElement.getProject(), newStringContents));
+    return myElement;
+  }
+}
diff --git a/base/src/com/google/idea/blaze/base/lang/buildfile/references/FileLookupData.java b/base/src/com/google/idea/blaze/base/lang/buildfile/references/FileLookupData.java
index eabe428..36ae50c 100644
--- a/base/src/com/google/idea/blaze/base/lang/buildfile/references/FileLookupData.java
+++ b/base/src/com/google/idea/blaze/base/lang/buildfile/references/FileLookupData.java
@@ -19,6 +19,7 @@
 import com.google.idea.blaze.base.lang.buildfile.psi.BuildFile;
 import com.google.idea.blaze.base.lang.buildfile.psi.StringLiteral;
 import com.google.idea.blaze.base.lang.buildfile.search.BlazePackage;
+import com.google.idea.blaze.base.model.primitives.Label;
 import com.google.idea.blaze.base.model.primitives.WorkspacePath;
 import com.google.idea.blaze.base.settings.Blaze;
 import com.intellij.icons.AllIcons;
@@ -32,8 +33,8 @@
 import com.intellij.util.PathUtil;
 import com.intellij.util.PlatformIcons;
 import icons.BlazeIcons;
+import javax.annotation.Nullable;
 import javax.swing.Icon;
-import org.jetbrains.annotations.Nullable;
 
 /** The data relevant to finding file lookups. */
 public class FileLookupData {
@@ -89,11 +90,14 @@
       StringLiteral element,
       @Nullable BuildFile basePackage,
       @Nullable VirtualFileFilter fileFilter) {
-    String basePackagePath =
-        basePackage != null ? basePackage.getWorkspaceRelativePackagePath() : null;
-    if (basePackagePath == null) {
+    if (basePackage == null) {
       return null;
     }
+    Label packageLabel = basePackage.getPackageLabel();
+    if (packageLabel == null) {
+      return null;
+    }
+    String basePackagePath = packageLabel.blazePackage().relativePath();
     String filePath = basePackagePath + "/" + LabelUtils.getRuleComponent(originalLabel);
     return new FileLookupData(
         originalLabel,
diff --git a/base/src/com/google/idea/blaze/base/lang/buildfile/references/FuncallReference.java b/base/src/com/google/idea/blaze/base/lang/buildfile/references/FuncallReference.java
index 526e2ec..b3d69dd 100644
--- a/base/src/com/google/idea/blaze/base/lang/buildfile/references/FuncallReference.java
+++ b/base/src/com/google/idea/blaze/base/lang/buildfile/references/FuncallReference.java
@@ -31,7 +31,7 @@
 public class FuncallReference extends PsiReferenceBase<FuncallExpression> {
 
   public FuncallReference(FuncallExpression element, TextRange rangeInElement) {
-    super(element, rangeInElement, /*soft*/ true);
+    super(element, rangeInElement, /* soft */ true);
   }
 
   @Nullable
diff --git a/base/src/com/google/idea/blaze/base/lang/buildfile/references/GlobReference.java b/base/src/com/google/idea/blaze/base/lang/buildfile/references/GlobReference.java
index c3ddb46..1f349c1 100644
--- a/base/src/com/google/idea/blaze/base/lang/buildfile/references/GlobReference.java
+++ b/base/src/com/google/idea/blaze/base/lang/buildfile/references/GlobReference.java
@@ -17,7 +17,7 @@
 
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.Lists;
-import com.google.idea.blaze.base.io.FileAttributeProvider;
+import com.google.idea.blaze.base.bazel.BuildSystemProvider;
 import com.google.idea.blaze.base.lang.buildfile.globbing.UnixGlob;
 import com.google.idea.blaze.base.lang.buildfile.psi.BuildFile;
 import com.google.idea.blaze.base.lang.buildfile.psi.Expression;
@@ -25,7 +25,9 @@
 import com.google.idea.blaze.base.lang.buildfile.psi.ListLiteral;
 import com.google.idea.blaze.base.lang.buildfile.psi.StringLiteral;
 import com.google.idea.blaze.base.lang.buildfile.psi.util.PsiUtils;
+import com.google.idea.blaze.base.settings.Blaze;
 import com.intellij.openapi.diagnostic.Logger;
+import com.intellij.openapi.project.Project;
 import com.intellij.openapi.util.TextRange;
 import com.intellij.psi.PsiElement;
 import com.intellij.psi.PsiElementResolveResult;
@@ -109,19 +111,19 @@
     if (includes.isEmpty()) {
       return ResolveResult.EMPTY_ARRAY;
     }
-
+    Project project = element.getProject();
     try {
       List<File> files =
           UnixGlob.forPath(containingDirectory)
               .addPatterns(includes)
               .addExcludes(excludes)
               .setExcludeDirectories(directoriesExcluded)
-              .setDirectoryFilter(directoryFilter(containingDirectory.getPath()))
+              .setDirectoryFilter(directoryFilter(project, containingDirectory.getPath()))
               .glob();
+
       List<ResolveResult> results = Lists.newArrayListWithCapacity(files.size());
       for (File file : files) {
-        PsiFileSystemItem psiFile =
-            BuildReferenceManager.getInstance(element.getProject()).resolveFile(file);
+        PsiFileSystemItem psiFile = BuildReferenceManager.getInstance(project).resolveFile(file);
         if (psiFile != null) {
           results.add(new PsiElementResolveResult(psiFile));
         }
@@ -133,14 +135,14 @@
     }
   }
 
-  private static Predicate<File> directoryFilter(String base) {
+  /** Don't traverse sub-directories which are themselves blaze packages */
+  private static Predicate<File> directoryFilter(Project project, String base) {
+    BuildSystemProvider provider = Blaze.getBuildSystemProvider(project);
     return file -> {
       if (base.equals(file.getPath())) {
         return true;
       }
-      File child = new File(file, "BUILD");
-      FileAttributeProvider attributeProvider = FileAttributeProvider.getInstance();
-      return !attributeProvider.exists(child) || attributeProvider.isDirectory(child);
+      return provider.findBuildFileInDirectory(file) == null;
     };
   }
 
diff --git a/base/src/com/google/idea/blaze/base/lang/buildfile/references/LabelReference.java b/base/src/com/google/idea/blaze/base/lang/buildfile/references/LabelReference.java
index f1e6542..a3457bb 100644
--- a/base/src/com/google/idea/blaze/base/lang/buildfile/references/LabelReference.java
+++ b/base/src/com/google/idea/blaze/base/lang/buildfile/references/LabelReference.java
@@ -35,8 +35,7 @@
 import com.intellij.psi.PsiReferenceBase;
 import com.intellij.util.ArrayUtil;
 import com.intellij.util.IncorrectOperationException;
-import org.jetbrains.annotations.NotNull;
-import org.jetbrains.annotations.Nullable;
+import javax.annotation.Nullable;
 
 /** Converts a blaze label into an absolute path, then resolves that path to a PsiElements */
 public class LabelReference extends PsiReferenceBase<StringLiteral> {
@@ -51,7 +50,7 @@
     /* Possibilities:
      * - target
      * - data file (.java, .txt, etc.)
-     * - glob contents (not yet handling globs)
+     * - glob contents
      */
     return resolveTarget(myElement.getStringContents());
   }
@@ -65,8 +64,8 @@
     if (!validLabelLocation(myElement)) {
       return null;
     }
-    if (!labelString.startsWith("//") && insideSkylarkExtension(myElement)) {
-      return getReferenceManager().resolveLabel(label.blazePackage(), label.targetName(), true);
+    if (!LabelUtils.isAbsolute(labelString) && insideSkylarkExtension(myElement)) {
+      return getReferenceManager().resolveLabel(label, true);
     }
     return getReferenceManager().resolveLabel(label);
   }
@@ -86,7 +85,6 @@
     return true;
   }
 
-  @NotNull
   @Override
   public Object[] getVariants() {
     if (!validLabelLocation(myElement)) {
@@ -110,7 +108,8 @@
     }
     String self = null;
     if (referencedBuildFile == myElement.getContainingFile()) {
-      FuncallExpression funcall = PsiUtils.getParentOfType(myElement, FuncallExpression.class);
+      FuncallExpression funcall =
+          PsiUtils.getParentOfType(myElement, FuncallExpression.class, true);
       if (funcall != null) {
         self = funcall.getName();
       }
@@ -242,8 +241,7 @@
       return null;
     }
     BlazePackage blazePackage = myElement.getBlazePackage();
-    return LabelUtils.createLabelFromString(
-        blazePackage != null ? blazePackage.buildFile : null, labelString);
+    return LabelUtils.createLabelFromString(blazePackage, labelString);
   }
 
   private static boolean skylarkExtensionReference(StringLiteral element) {
diff --git a/base/src/com/google/idea/blaze/base/lang/buildfile/references/LabelUtils.java b/base/src/com/google/idea/blaze/base/lang/buildfile/references/LabelUtils.java
index db58991..cf3e8df 100644
--- a/base/src/com/google/idea/blaze/base/lang/buildfile/references/LabelUtils.java
+++ b/base/src/com/google/idea/blaze/base/lang/buildfile/references/LabelUtils.java
@@ -54,8 +54,8 @@
     if (blazePackage == null || ruleName == null) {
       return null;
     }
-    WorkspacePath packagePath = blazePackage.buildFile.getPackageWorkspacePath();
-    return createLabelFromRuleName(packagePath, ruleName);
+    Label packageLabel = blazePackage.buildFile.getPackageLabel();
+    return packageLabel != null ? packageLabel.withTargetName(ruleName) : null;
   }
 
   public static Label createLabelFromRuleName(
@@ -67,36 +67,35 @@
     if (packagePath == null || name == null) {
       return null;
     }
-    // TODO: Is Label too inefficient?
-    // (validation done twice,creating List during constructor,
-    // re-parsing to extract the package/rule each time)
-    return new Label(packagePath, name);
+    return Label.create(packagePath, name);
   }
 
   /**
-   * Canonicalizes the label (to the form //packagePath:packageRelativeTarget). Returns null if the
-   * string does not represent a valid label.
+   * Canonicalizes the label (to the form [@external_workspace]//packagePath:packageRelativeTarget).
+   * Returns null if the string does not represent a valid label.
    */
   @Nullable
   public static Label createLabelFromString(
-      @Nullable BuildFile file, @Nullable String labelString) {
+      @Nullable BlazePackage blazePackage, @Nullable String labelString) {
     if (labelString == null) {
       return null;
     }
     int colonIndex = labelString.indexOf(':');
-    if (labelString.startsWith("//")) {
+    if (isAbsolute(labelString)) {
       if (colonIndex == -1) {
         // add the implicit rule name
         labelString += ":" + PathUtil.getFileName(labelString);
       }
       return Label.createIfValid(labelString);
     }
-    WorkspacePath packagePath = file != null ? file.getPackageWorkspacePath() : null;
-    if (packagePath == null) {
+    // package-relative label of the form '[:]relativePath'
+    if (colonIndex > 0 || blazePackage == null) {
       return null;
     }
-    String localPath = colonIndex == -1 ? labelString : labelString.substring(1);
-    return Label.createIfValid("//" + packagePath.relativePath() + ":" + localPath);
+    Label packageLabel = blazePackage.getPackageLabel();
+    return packageLabel != null
+        ? packageLabel.withTargetName(labelString.substring(colonIndex + 1))
+        : null;
   }
 
   /** The blaze file referenced by the label. */
@@ -121,12 +120,33 @@
     return labelString.startsWith(":") ? labelString.substring(1) : labelString;
   }
 
+  /** For a label of the form '[@ext]//package/path:target/name', returns '//package/path' */
   public static String getPackagePathComponent(String labelString) {
-    if (!labelString.startsWith("//")) {
+    if (!isAbsolute(labelString)) {
+      return "";
+    }
+    int slashesIndex = labelString.indexOf("//");
+    if (slashesIndex == -1) {
       return "";
     }
     int colonIndex = labelString.indexOf(':');
-    return colonIndex == -1 ? labelString : labelString.substring(0, colonIndex);
+    return colonIndex == -1
+        ? labelString.substring(slashesIndex)
+        : labelString.substring(slashesIndex, colonIndex);
+  }
+
+  @Nullable
+  public static String getExternalWorkspaceComponent(String labelString) {
+    if (!labelString.startsWith("@")) {
+      return null;
+    }
+    int slashesIndex = labelString.indexOf("//");
+    return slashesIndex == -1 ? null : labelString.substring(1, slashesIndex);
+  }
+
+  /** Returns false for package-relative labels */
+  public static boolean isAbsolute(String labelString) {
+    return labelString.startsWith("//") || labelString.startsWith("@");
   }
 
   /** 'load' reference. Of the form [path][/ or :][extra_path/]file_name.bzl */
diff --git a/base/src/com/google/idea/blaze/base/lang/buildfile/references/LoadedSymbolReference.java b/base/src/com/google/idea/blaze/base/lang/buildfile/references/LoadedSymbolReference.java
index 219d843..44fa77f 100644
--- a/base/src/com/google/idea/blaze/base/lang/buildfile/references/LoadedSymbolReference.java
+++ b/base/src/com/google/idea/blaze/base/lang/buildfile/references/LoadedSymbolReference.java
@@ -33,7 +33,7 @@
   private final LabelReference bzlFileReference;
 
   public LoadedSymbolReference(StringLiteral element, LabelReference bzlFileReference) {
-    super(element, new TextRange(0, element.getTextLength()), /*soft*/ false);
+    super(element, new TextRange(0, element.getTextLength()), /* soft */ false);
     this.bzlFileReference = bzlFileReference;
   }
 
@@ -54,7 +54,7 @@
       return EMPTY_ARRAY;
     }
     CompletionResultsProcessor processor =
-        new CompletionResultsProcessor(myElement, myElement.getQuoteType());
+        new CompletionResultsProcessor(myElement, myElement.getQuoteType(), false);
     ((BuildFile) bzlFile).searchSymbolsInScope(processor, null);
     return processor.getResults().toArray();
   }
diff --git a/base/src/com/google/idea/blaze/base/lang/buildfile/references/LocalReference.java b/base/src/com/google/idea/blaze/base/lang/buildfile/references/LocalReference.java
index 85808b1..08b3237 100644
--- a/base/src/com/google/idea/blaze/base/lang/buildfile/references/LocalReference.java
+++ b/base/src/com/google/idea/blaze/base/lang/buildfile/references/LocalReference.java
@@ -33,7 +33,7 @@
 public class LocalReference extends PsiReferenceBase<ReferenceExpression> {
 
   public LocalReference(ReferenceExpression element) {
-    super(element, new TextRange(0, element.getTextLength()), /*soft*/ false);
+    super(element, new TextRange(0, element.getTextLength()), /* soft */ false);
   }
 
   @Nullable
@@ -49,7 +49,7 @@
   @Override
   public Object[] getVariants() {
     CompletionResultsProcessor processor =
-        new CompletionResultsProcessor(myElement, QuoteType.NoQuotes);
+        new CompletionResultsProcessor(myElement, QuoteType.NoQuotes, true);
     ResolveUtil.searchInScope(myElement, processor);
     return processor.getResults().toArray();
   }
diff --git a/base/src/com/google/idea/blaze/base/lang/buildfile/references/PackageReferenceFragment.java b/base/src/com/google/idea/blaze/base/lang/buildfile/references/PackageReferenceFragment.java
index 6dba496..746d4e5 100644
--- a/base/src/com/google/idea/blaze/base/lang/buildfile/references/PackageReferenceFragment.java
+++ b/base/src/com/google/idea/blaze/base/lang/buildfile/references/PackageReferenceFragment.java
@@ -18,6 +18,7 @@
 import com.google.idea.blaze.base.lang.buildfile.psi.BuildFile;
 import com.google.idea.blaze.base.lang.buildfile.psi.StringLiteral;
 import com.google.idea.blaze.base.lang.buildfile.psi.util.PsiUtils;
+import com.google.idea.blaze.base.model.primitives.Label;
 import com.google.idea.blaze.base.model.primitives.WorkspacePath;
 import com.intellij.lang.ASTNode;
 import com.intellij.openapi.util.TextRange;
@@ -27,7 +28,7 @@
 import com.intellij.util.PathUtil;
 import javax.annotation.Nullable;
 
-/** The label component preceeding the colon. */
+/** The label component preceding the colon. */
 public class PackageReferenceFragment extends PsiReferenceBase<StringLiteral> {
 
   public PackageReferenceFragment(LabelReference labelReference) {
@@ -84,10 +85,11 @@
     if (element.equals(resolve())) {
       return myElement;
     }
-    WorkspacePath newPath = ((BuildFile) element).getPackageWorkspacePath();
-    if (newPath == null) {
+    Label newPackageLabel = ((BuildFile) element).getPackageLabel();
+    if (newPackageLabel == null) {
       return myElement;
     }
+    String newPath = newPackageLabel.blazePackage().toString();
     String labelString = myElement.getStringContents();
     int colonIndex = labelString.indexOf(':');
     if (colonIndex != -1) {
diff --git a/base/src/com/google/idea/blaze/base/lang/buildfile/references/TargetReference.java b/base/src/com/google/idea/blaze/base/lang/buildfile/references/TargetReference.java
index 52ee6da..68ba606 100644
--- a/base/src/com/google/idea/blaze/base/lang/buildfile/references/TargetReference.java
+++ b/base/src/com/google/idea/blaze/base/lang/buildfile/references/TargetReference.java
@@ -34,7 +34,7 @@
 public class TargetReference extends PsiReferenceBase<TargetExpression> {
 
   public TargetReference(TargetExpression element) {
-    super(element, new TextRange(0, element.getTextLength()), /*soft*/ true);
+    super(element, new TextRange(0, element.getTextLength()), /* soft */ true);
   }
 
   @Nullable
@@ -51,7 +51,7 @@
   @Override
   public Object[] getVariants() {
     CompletionResultsProcessor processor =
-        new CompletionResultsProcessor(myElement, QuoteType.NoQuotes);
+        new CompletionResultsProcessor(myElement, QuoteType.NoQuotes, true);
     ResolveUtil.searchInScope(myElement, processor);
     return processor.getResults().toArray();
   }
diff --git a/base/src/com/google/idea/blaze/base/lang/buildfile/search/BlazePackage.java b/base/src/com/google/idea/blaze/base/lang/buildfile/search/BlazePackage.java
index 6f1b102..94f2f48 100644
--- a/base/src/com/google/idea/blaze/base/lang/buildfile/search/BlazePackage.java
+++ b/base/src/com/google/idea/blaze/base/lang/buildfile/search/BlazePackage.java
@@ -15,10 +15,13 @@
  */
 package com.google.idea.blaze.base.lang.buildfile.search;
 
+import com.google.idea.blaze.base.bazel.BuildSystemProvider;
 import com.google.idea.blaze.base.lang.buildfile.psi.BuildFile;
-import com.google.idea.blaze.base.model.primitives.WorkspacePath;
+import com.google.idea.blaze.base.model.primitives.Label;
 import com.google.idea.blaze.base.settings.Blaze;
+import com.google.idea.blaze.base.sync.workspace.WorkspaceHelper;
 import com.intellij.history.core.Paths;
+import com.intellij.openapi.roots.ProjectFileIndex;
 import com.intellij.openapi.vfs.VirtualFile;
 import com.intellij.psi.PackagePrefixFileSystemItem;
 import com.intellij.psi.PsiDirectory;
@@ -30,7 +33,10 @@
 import java.util.Objects;
 import javax.annotation.Nullable;
 
-/** Defines the files accessible by a given blaze package. */
+/**
+ * A Blaze package is a directory containing a BUILD file, plus all subdirectories which aren't
+ * themselves Blaze packages.
+ */
 public class BlazePackage {
 
   @Nullable
@@ -96,6 +102,11 @@
     return new BlazePackageSearchScope(this, onlyBlazeFiles);
   }
 
+  @Nullable
+  public Label getPackageLabel() {
+    return WorkspaceHelper.getBuildLabel(buildFile.getProject(), buildFile.getFile());
+  }
+
   /**
    * Returns the file path relative to this blaze package, or null if it does lie inside this
    * package
@@ -108,19 +119,13 @@
 
   /** Formats the child file path as a BUILD label (i.e. "//package_path[:relative_path]") */
   @Nullable
-  public String getBuildLabelForChild(String filePath) {
-    WorkspacePath packagePath = buildFile.getPackageWorkspacePath();
-    if (packagePath == null) {
+  public Label getBuildLabelForChild(String filePath) {
+    Label parentPackage = getPackageLabel();
+    if (parentPackage == null) {
       return null;
     }
     String relativePath = getPackageRelativePath(filePath);
-    if (relativePath == null) {
-      return null;
-    }
-    if (relativePath.isEmpty()) {
-      return "//" + packagePath;
-    }
-    return "//" + packagePath + ":" + relativePath;
+    return parentPackage.withTargetName(relativePath);
   }
 
   /**
@@ -190,6 +195,29 @@
   public String toString() {
     return String.format(
         "%s package: %s",
-        Blaze.buildSystemName(buildFile.getProject()), buildFile.getPackageWorkspacePath());
+        Blaze.buildSystemName(buildFile.getProject()), buildFile.getPackageLabel());
+  }
+
+  public static boolean hasBlazePackageChild(PsiDirectory directory) {
+    ProjectFileIndex index = ProjectFileIndex.SERVICE.getInstance(directory.getProject());
+    BuildSystemProvider buildSystemProvider = Blaze.getBuildSystemProvider(directory.getProject());
+    return hasBlazePackageChild(index, buildSystemProvider, directory);
+  }
+
+  private static boolean hasBlazePackageChild(
+      ProjectFileIndex index, BuildSystemProvider buildSystemProvider, PsiDirectory directory) {
+    if (!index.isInSourceContent(directory.getVirtualFile())) {
+      // only search project files
+      return false;
+    }
+    if (buildSystemProvider.findBuildFileInDirectory(directory.getVirtualFile()) != null) {
+      return true;
+    }
+    for (PsiDirectory child : directory.getSubdirectories()) {
+      if (hasBlazePackageChild(index, buildSystemProvider, child)) {
+        return true;
+      }
+    }
+    return false;
   }
 }
diff --git a/base/src/com/google/idea/blaze/base/lang/buildfile/search/BlazePackageSearchScope.java b/base/src/com/google/idea/blaze/base/lang/buildfile/search/BlazePackageSearchScope.java
index d5f6606..841ad88 100644
--- a/base/src/com/google/idea/blaze/base/lang/buildfile/search/BlazePackageSearchScope.java
+++ b/base/src/com/google/idea/blaze/base/lang/buildfile/search/BlazePackageSearchScope.java
@@ -65,7 +65,7 @@
   public String toString() {
     return String.format(
         "%s directory scope: %s",
-        Blaze.buildSystemName(getProject()), blazePackage.buildFile.getPackageWorkspacePath());
+        Blaze.buildSystemName(getProject()), blazePackage.buildFile.getPackageLabel());
   }
 
   @Override
diff --git a/base/src/com/google/idea/blaze/base/lang/buildfile/search/BuildLabelReferenceSearcher.java b/base/src/com/google/idea/blaze/base/lang/buildfile/search/BuildReferenceSearcher.java
similarity index 78%
rename from base/src/com/google/idea/blaze/base/lang/buildfile/search/BuildLabelReferenceSearcher.java
rename to base/src/com/google/idea/blaze/base/lang/buildfile/search/BuildReferenceSearcher.java
index 688cc95..d35fc90 100644
--- a/base/src/com/google/idea/blaze/base/lang/buildfile/search/BuildLabelReferenceSearcher.java
+++ b/base/src/com/google/idea/blaze/base/lang/buildfile/search/BuildReferenceSearcher.java
@@ -20,11 +20,10 @@
 import com.google.idea.blaze.base.lang.buildfile.psi.BuildFile;
 import com.google.idea.blaze.base.lang.buildfile.psi.BuildFile.BlazeFileType;
 import com.google.idea.blaze.base.lang.buildfile.psi.FuncallExpression;
-import com.google.idea.blaze.base.lang.buildfile.psi.FunctionStatement;
+import com.google.idea.blaze.base.lang.buildfile.psi.NamedBuildElement;
 import com.google.idea.blaze.base.lang.buildfile.psi.util.PsiUtils;
 import com.google.idea.blaze.base.lang.buildfile.references.LabelUtils;
 import com.google.idea.blaze.base.model.primitives.Label;
-import com.google.idea.blaze.base.model.primitives.WorkspacePath;
 import com.intellij.openapi.application.QueryExecutorBase;
 import com.intellij.psi.PsiDirectory;
 import com.intellij.psi.PsiElement;
@@ -39,41 +38,45 @@
 import java.util.List;
 import javax.annotation.Nullable;
 
-/** String search for label references in BUILD files */
-public class BuildLabelReferenceSearcher extends QueryExecutorBase<PsiReference, SearchParameters> {
+/** String search for references in BUILD files */
+public class BuildReferenceSearcher extends QueryExecutorBase<PsiReference, SearchParameters> {
 
-  public BuildLabelReferenceSearcher() {
+  public BuildReferenceSearcher() {
     super(true);
   }
 
   @Override
   public void processQuery(SearchParameters params, Processor<PsiReference> consumer) {
     PsiElement element = params.getElementToSearch();
-    if (element instanceof FunctionStatement) {
-      String fnName = ((FunctionStatement) element).getName();
+    if (element instanceof NamedBuildElement) {
+      String fnName = ((NamedBuildElement) element).getName();
       if (fnName != null) {
         searchForString(params, element, fnName);
       }
       return;
     }
+
     PsiFile file = ResolveUtil.asFileSearch(element);
     if (file != null) {
       processFileReferences(params, file);
       return;
     }
-
     if (!(element instanceof FuncallExpression)) {
       return;
     }
-
-    Label label = ((FuncallExpression) element).resolveBuildLabel();
+    FuncallExpression funcall = (FuncallExpression) element;
     PsiFile localFile = element.getContainingFile();
-    if (label == null || localFile == null) {
+    if (localFile == null) {
+      return;
+    }
+    Label label = funcall.resolveBuildLabel();
+    if (label == null) {
+      searchForExternalWorkspace(params, localFile, funcall);
       return;
     }
     List<String> stringsToSearch = LabelUtils.getAllValidLabelStrings(label, true);
     for (String string : stringsToSearch) {
-      if (string.startsWith("//")) {
+      if (LabelUtils.isAbsolute(string)) {
         searchForString(params, element, string);
       } else {
         // only a valid reference from local package -- restrict the search scope accordingly
@@ -117,17 +120,18 @@
 
   /** Find references to both the file itself, and build targets defined in the file. */
   private void processBuildFileReferences(SearchParameters params, BuildFile file) {
-    WorkspacePath workspacePath = file.getPackageWorkspacePath();
-    if (workspacePath == null) {
+    Label label = file.getBuildLabel();
+    if (label == null) {
       return;
     }
+    String labelString = label.toString();
     List<String> stringsToSearch = Lists.newArrayList();
     if (file.getBlazeFileType() == BlazeFileType.BuildPackage) {
-      stringsToSearch.add("//" + workspacePath);
+      // remove ':__pkg__' component of label
+      stringsToSearch.add(labelString.split(":", 2)[0]);
     } else {
-      stringsToSearch.add("//" + workspacePath + ":" + file.getName());
-      stringsToSearch.add(
-          "//" + workspacePath + "/" + file.getName()); // deprecated load/subinclude format
+      stringsToSearch.add(labelString);
+      stringsToSearch.add(labelString.replace(':', '/')); // deprecated load/subinclude format
     }
     for (String string : stringsToSearch) {
       searchForString(params, file, string);
@@ -161,4 +165,21 @@
     }
     params.getOptimizer().searchWord(string, scope, UsageSearchContext.IN_STRINGS, true, element);
   }
+
+  private static void searchForExternalWorkspace(
+      SearchParameters params, PsiFile file, FuncallExpression funcall) {
+    if (!isBlazeWorkspaceFile(file)) {
+      return;
+    }
+    String name = funcall.getNameArgumentValue();
+    if (name != null) {
+      searchForString(params, funcall, "@" + name);
+    }
+  }
+
+  /** Is the file a blaze WORKSPACE file */
+  private static boolean isBlazeWorkspaceFile(PsiFile file) {
+    return file instanceof BuildFile
+        && ((BuildFile) file).getBlazeFileType() == BlazeFileType.Workspace;
+  }
 }
diff --git a/base/src/com/google/idea/blaze/base/lang/buildfile/stubs/BuildFileStubBuilder.java b/base/src/com/google/idea/blaze/base/lang/buildfile/stubs/BuildFileStubBuilder.java
index 73d1e58..4c4a53a 100644
--- a/base/src/com/google/idea/blaze/base/lang/buildfile/stubs/BuildFileStubBuilder.java
+++ b/base/src/com/google/idea/blaze/base/lang/buildfile/stubs/BuildFileStubBuilder.java
@@ -19,7 +19,7 @@
 import com.intellij.psi.stubs.BinaryFileStubBuilder;
 import com.intellij.psi.stubs.Stub;
 import com.intellij.util.indexing.FileContent;
-import org.jetbrains.annotations.Nullable;
+import javax.annotation.Nullable;
 
 /** Empty stub builder to suppress errors when IntelliJ is looking for stubs. */
 public class BuildFileStubBuilder implements BinaryFileStubBuilder {
diff --git a/base/src/com/google/idea/blaze/base/lang/buildfile/sync/BuildLangSyncPlugin.java b/base/src/com/google/idea/blaze/base/lang/buildfile/sync/BuildLangSyncPlugin.java
index 0279a2f..4a0f12b 100644
--- a/base/src/com/google/idea/blaze/base/lang/buildfile/sync/BuildLangSyncPlugin.java
+++ b/base/src/com/google/idea/blaze/base/lang/buildfile/sync/BuildLangSyncPlugin.java
@@ -15,9 +15,10 @@
  */
 package com.google.idea.blaze.base.lang.buildfile.sync;
 
-import com.google.common.collect.ImmutableList;
 import com.google.common.util.concurrent.ListenableFuture;
+import com.google.idea.blaze.base.command.BlazeFlags;
 import com.google.idea.blaze.base.command.info.BlazeInfo;
+import com.google.idea.blaze.base.command.info.BlazeInfoRunner;
 import com.google.idea.blaze.base.ideinfo.TargetMap;
 import com.google.idea.blaze.base.lang.buildfile.language.semantics.BuildLanguageSpec;
 import com.google.idea.blaze.base.model.SyncState;
@@ -30,7 +31,6 @@
 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.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 com.google.repackaged.devtools.build.lib.query2.proto.proto2api.Build;
@@ -53,7 +53,7 @@
       WorkspaceRoot workspaceRoot,
       ProjectViewSet projectViewSet,
       WorkspaceLanguageSettings workspaceLanguageSettings,
-      BlazeRoots blazeRoots,
+      BlazeInfo blazeInfo,
       @Nullable WorkingSet workingSet,
       WorkspacePathResolver workspacePathResolver,
       ArtifactLocationDecoder artifactLocationDecoder,
@@ -62,7 +62,7 @@
       @Nullable SyncState previousSyncState) {
 
     LanguageSpecResult spec =
-        getBuildLanguageSpec(project, workspaceRoot, previousSyncState, context);
+        getBuildLanguageSpec(project, workspaceRoot, projectViewSet, previousSyncState, context);
     if (spec != null) {
       syncStateBuilder.put(LanguageSpecResult.class, spec);
     }
@@ -72,6 +72,7 @@
   private static LanguageSpecResult getBuildLanguageSpec(
       Project project,
       WorkspaceRoot workspace,
+      ProjectViewSet projectViewSet,
       @Nullable SyncState previousSyncState,
       BlazeContext parentContext) {
     LanguageSpecResult oldResult =
@@ -84,7 +85,8 @@
             parentContext,
             (context) -> {
               context.push(new TimingScope("BUILD language spec"));
-              BuildLanguageSpec spec = parseLanguageSpec(project, workspace, context);
+              BuildLanguageSpec spec =
+                  parseLanguageSpec(project, workspace, projectViewSet, context);
               if (spec != null) {
                 return new LanguageSpecResult(spec, System.currentTimeMillis());
               }
@@ -95,17 +97,20 @@
 
   @Nullable
   private static BuildLanguageSpec parseLanguageSpec(
-      Project project, WorkspaceRoot workspace, BlazeContext context) {
+      Project project,
+      WorkspaceRoot workspace,
+      ProjectViewSet projectViewSet,
+      BlazeContext context) {
     try {
       // it's wasteful converting to a string and back, but uses existing code,
       // and has a very minor cost (this is only run once per workspace)
       ListenableFuture<byte[]> future =
-          BlazeInfo.getInstance()
+          BlazeInfoRunner.getInstance()
               .runBlazeInfoGetBytes(
                   context,
-                  Blaze.getBuildSystem(project),
+                  Blaze.getBuildSystemProvider(project).getSyncBinaryPath(),
                   workspace,
-                  ImmutableList.of(),
+                  BlazeFlags.buildFlags(project, projectViewSet),
                   BlazeInfo.BUILD_LANGUAGE);
 
       return BuildLanguageSpec.fromProto(Build.BuildLanguage.parseFrom(future.get()));
diff --git a/base/src/com/google/idea/blaze/base/lang/buildfile/validation/BuildAnnotator.java b/base/src/com/google/idea/blaze/base/lang/buildfile/validation/BuildAnnotator.java
index 84028f8..b4b9230 100644
--- a/base/src/com/google/idea/blaze/base/lang/buildfile/validation/BuildAnnotator.java
+++ b/base/src/com/google/idea/blaze/base/lang/buildfile/validation/BuildAnnotator.java
@@ -16,6 +16,7 @@
 package com.google.idea.blaze.base.lang.buildfile.validation;
 
 import com.google.idea.blaze.base.lang.buildfile.psi.BuildElementVisitor;
+import com.intellij.lang.annotation.Annotation;
 import com.intellij.lang.annotation.AnnotationHolder;
 import com.intellij.lang.annotation.Annotator;
 import com.intellij.psi.PsiElement;
@@ -39,7 +40,11 @@
     }
   }
 
-  protected void markError(PsiElement element, String message) {
-    getHolder().createErrorAnnotation(element, message);
+  protected Annotation markError(PsiElement element, String message) {
+    return getHolder().createErrorAnnotation(element, message);
+  }
+
+  protected Annotation markWarning(PsiElement element, String message) {
+    return getHolder().createWarningAnnotation(element, message);
   }
 }
diff --git a/base/src/com/google/idea/blaze/base/lang/buildfile/validation/BuildElementValidation.java b/base/src/com/google/idea/blaze/base/lang/buildfile/validation/BuildElementValidation.java
index 138908c..d8a8206 100644
--- a/base/src/com/google/idea/blaze/base/lang/buildfile/validation/BuildElementValidation.java
+++ b/base/src/com/google/idea/blaze/base/lang/buildfile/validation/BuildElementValidation.java
@@ -15,6 +15,7 @@
  */
 package com.google.idea.blaze.base.lang.buildfile.validation;
 
+import com.google.common.collect.ImmutableList;
 import com.google.idea.blaze.base.lang.buildfile.psi.DictionaryLiteral;
 import com.google.idea.blaze.base.lang.buildfile.psi.FunctionStatement;
 import com.google.idea.blaze.base.lang.buildfile.psi.GlobExpression;
@@ -38,14 +39,21 @@
   private static final EnumSet<Build.Attribute.Discriminator> LIST_TYPES =
       EnumSet.of(
           Discriminator.STRING_LIST,
+          Discriminator.DISTRIBUTION_SET,
           Discriminator.LABEL_LIST,
           Discriminator.OUTPUT_LIST,
           Discriminator.FILESET_ENTRY_LIST,
           Discriminator.INTEGER_LIST,
-          Discriminator.LICENSE);
+          Discriminator.LICENSE,
+          Discriminator.SELECTOR_LIST);
 
   private static final EnumSet<Build.Attribute.Discriminator> DICT_TYPES =
-      EnumSet.of(Discriminator.LABEL_LIST_DICT, Discriminator.STRING_LIST_DICT);
+      EnumSet.of(
+          Discriminator.LABEL_LIST_DICT,
+          Discriminator.LABEL_KEYED_STRING_DICT,
+          Discriminator.STRING_DICT,
+          Discriminator.STRING_LIST_DICT,
+          Discriminator.LABEL_DICT_UNARY);
 
   private static final EnumSet<Build.Attribute.Discriminator> STRING_TYPES =
       EnumSet.of(
@@ -58,9 +66,20 @@
   private static final EnumSet<Build.Attribute.Discriminator> INTEGER_TYPES =
       EnumSet.of(Discriminator.INTEGER, Discriminator.BOOLEAN, Discriminator.TRISTATE);
 
+  // This enum list is duplicated several times through Bazel source code. In some places there are
+  // additional items not covered here. Don't show spurious errors when more items are added.
+  private static final EnumSet<Build.Attribute.Discriminator> HANDLED_TYPES =
+      EnumSet.copyOf(
+          ImmutableList.<Discriminator>builder()
+              .addAll(LIST_TYPES)
+              .addAll(DICT_TYPES)
+              .addAll(STRING_TYPES)
+              .addAll(INTEGER_TYPES)
+              .build());
+
   /** Returns false iff we know with certainty that the element cannot resolve to the given type. */
   public static boolean possiblyValidType(PsiElement element, Build.Attribute.Discriminator type) {
-    if (type == Discriminator.UNKNOWN) {
+    if (!HANDLED_TYPES.contains(type)) {
       return true;
     }
     if (element instanceof ListLiteral || element instanceof GlobExpression) {
diff --git a/base/src/com/google/idea/blaze/base/lang/buildfile/validation/BuiltInRuleAnnotator.java b/base/src/com/google/idea/blaze/base/lang/buildfile/validation/BuiltInRuleAnnotator.java
index 48f8136..8fa0362 100644
--- a/base/src/com/google/idea/blaze/base/lang/buildfile/validation/BuiltInRuleAnnotator.java
+++ b/base/src/com/google/idea/blaze/base/lang/buildfile/validation/BuiltInRuleAnnotator.java
@@ -43,8 +43,16 @@
     if (rule == null) {
       return;
     }
+    if (node.getReferencedElement() != null) {
+      // this has been locally overridden, so don't attempt validation
+      return;
+    }
     Set<String> missingAttributes = new TreeSet<>(rule.mandatoryAttributes.keySet());
     for (Argument arg : node.getArguments()) {
+      if (arg instanceof Argument.StarStar) {
+        missingAttributes.clear();
+        continue;
+      }
       String name = arg.getName();
       if (name == null) {
         continue;
diff --git a/base/src/com/google/idea/blaze/base/lang/buildfile/validation/GlobPatternValidator.java b/base/src/com/google/idea/blaze/base/lang/buildfile/validation/GlobPatternValidator.java
index c82d736..f039e31 100644
--- a/base/src/com/google/idea/blaze/base/lang/buildfile/validation/GlobPatternValidator.java
+++ b/base/src/com/google/idea/blaze/base/lang/buildfile/validation/GlobPatternValidator.java
@@ -20,16 +20,12 @@
 
 /**
  * Support for resolving globs.
- *
- * <p>
  */
 public class GlobPatternValidator {
 
   /**
    * Validate a single glob pattern. If it's invalid, returns an error message. Otherwise, returns
    * null.
-   *
-   * <p>
    */
   @Nullable
   public static String validate(String pattern) {
@@ -58,6 +54,7 @@
         case '[':
         case ']':
           return "illegal character '" + c + "'";
+        default: // fall out
       }
     }
     Iterable<String> segments = Splitter.on('/').split(pattern);
diff --git a/base/src/com/google/idea/blaze/base/lang/buildfile/validation/LoadErrorAnnotator.java b/base/src/com/google/idea/blaze/base/lang/buildfile/validation/LoadErrorAnnotator.java
deleted file mode 100644
index c8ea27a..0000000
--- a/base/src/com/google/idea/blaze/base/lang/buildfile/validation/LoadErrorAnnotator.java
+++ /dev/null
@@ -1,113 +0,0 @@
-/*
- * Copyright 2016 The Bazel Authors. All rights reserved.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *    http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.google.idea.blaze.base.lang.buildfile.validation;
-
-import com.google.idea.blaze.base.lang.buildfile.psi.Argument;
-import com.google.idea.blaze.base.lang.buildfile.psi.BuildElement;
-import com.google.idea.blaze.base.lang.buildfile.psi.BuildFile;
-import com.google.idea.blaze.base.lang.buildfile.psi.FuncallExpression;
-import com.google.idea.blaze.base.lang.buildfile.psi.FunctionStatement;
-import com.google.idea.blaze.base.lang.buildfile.psi.LoadStatement;
-import com.google.idea.blaze.base.lang.buildfile.psi.LoadedSymbol;
-import com.google.idea.blaze.base.lang.buildfile.psi.ParameterList;
-import com.google.idea.blaze.base.lang.buildfile.psi.StringLiteral;
-import com.google.idea.blaze.base.lang.buildfile.references.LabelReference;
-import com.intellij.psi.PsiElement;
-import java.util.Arrays;
-import javax.annotation.Nullable;
-
-/**
- * Error annotations for load statements, post parsing.
- *
- * <p>This has been turned off because it's unusable. BuildFile is re-parsed *every* time it's
- * touched, and is never cached. Until this is fixed, we can't run any annotators touching the file.
- *
- * <p>One option: try moving all expensive checks to 'visitFile', so they're not run in parallel
- */
-public class LoadErrorAnnotator extends BuildAnnotator {
-
-  @Override
-  public void visitLoadStatement(LoadStatement node) {
-    BuildElement[] children = node.buildElementChildren();
-    //    StringLiteral[] strings = node..getChildStrings();
-    if (children.length == 0) {
-      return;
-    }
-    PsiElement skylarkRef = getSkylarkRef(children[0]);
-    if (skylarkRef == null) {
-      markError(children[0], "Cannot find this Skylark module");
-      return;
-    }
-    if (!(skylarkRef instanceof BuildFile)) {
-      markError(children[0], children[0].getText() + " is not a Skylark module");
-      return;
-    }
-
-    LoadedSymbol[] symbols =
-        Arrays.stream(children)
-            .filter(element -> element instanceof LoadedSymbol)
-            .toArray(LoadedSymbol[]::new);
-    if (symbols.length == 1) {
-      markError(node, "No symbols imported from Skylark module");
-      return;
-    }
-    BuildFile skylarkModule = (BuildFile) skylarkRef;
-    for (int i = 0; i < symbols.length; i++) {
-      String text = symbols[i].getSymbolString();
-      if (text == null) {
-        continue;
-      }
-      FunctionStatement fn = skylarkModule.findDeclaredFunction(text);
-      if (fn == null) {
-        markError(
-            symbols[i],
-            "Function '" + text + "' not found in Skylark module " + skylarkModule.getFileName());
-      }
-    }
-  }
-
-  @Nullable
-  private static PsiElement getSkylarkRef(BuildElement firstChild) {
-    if (firstChild instanceof StringLiteral) {
-      return new LabelReference((StringLiteral) firstChild, false).resolve();
-    }
-    return null;
-  }
-
-  @Override
-  public void visitFuncallExpression(FuncallExpression node) {
-    FunctionStatement function = (FunctionStatement) node.getReferencedElement();
-    if (function == null) {
-      // likely a built-in rule.
-      return;
-    }
-    // check keyword args match function parameters
-    ParameterList params = function.getParameterList();
-    if (params == null || params.hasStarStar()) {
-      return;
-    }
-    for (Argument arg : node.getArguments()) {
-      if (arg instanceof Argument.Keyword) {
-        String name = arg.getName();
-        if (name != null && params.findParameterByName(name) == null) {
-          markError(
-              arg,
-              "No parameter found in '" + node.getFunctionName() + "' with name '" + name + "'");
-        }
-      }
-    }
-  }
-}
diff --git a/base/src/com/google/idea/blaze/base/lang/buildfile/validation/LoadStatementAnnotator.java b/base/src/com/google/idea/blaze/base/lang/buildfile/validation/LoadStatementAnnotator.java
new file mode 100644
index 0000000..c51ccc8
--- /dev/null
+++ b/base/src/com/google/idea/blaze/base/lang/buildfile/validation/LoadStatementAnnotator.java
@@ -0,0 +1,77 @@
+/*
+ * Copyright 2017 The Bazel Authors. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.idea.blaze.base.lang.buildfile.validation;
+
+import com.google.idea.blaze.base.lang.buildfile.psi.LoadStatement;
+import com.google.idea.blaze.base.lang.buildfile.psi.LoadedSymbol;
+import com.google.idea.blaze.base.lang.buildfile.psi.StringLiteral;
+import com.google.idea.blaze.base.lang.buildfile.quickfix.DeprecatedLoadQuickFix;
+import com.intellij.codeInspection.InspectionManager;
+import com.intellij.codeInspection.ProblemDescriptor;
+import com.intellij.codeInspection.ProblemHighlightType;
+import com.intellij.lang.annotation.Annotation;
+import javax.annotation.Nullable;
+
+/** Adds warning/error annotations to load statements. */
+public class LoadStatementAnnotator extends BuildAnnotator {
+
+  @Override
+  public void visitLoadStatement(LoadStatement node) {
+    validateImportTarget(node.getImportPsiElement());
+  }
+
+  @Override
+  public void visitLoadedSymbol(LoadedSymbol node) {
+    StringLiteral loadedString = node.getImport();
+    if (loadedString == null) {
+      return;
+    }
+    String name = loadedString.getStringContents();
+    if (name.startsWith("_")) {
+      markError(node, String.format("Symbol '%s' is private and cannot be imported.", name));
+    }
+  }
+
+  private void validateImportTarget(@Nullable StringLiteral target) {
+    if (target == null) {
+      return;
+    }
+    String targetString = target.getStringContents();
+    if (targetString == null
+        || targetString.startsWith(":")
+        || targetString.startsWith("//")
+        || targetString.startsWith("@")
+        || targetString.length() < 2) {
+      return;
+    }
+    if (targetString.startsWith("/")) {
+      Annotation annotation =
+          markWarning(
+              target, "Deprecated load syntax; loaded Skylark module should by in label format.");
+      InspectionManager inspectionManager = InspectionManager.getInstance(target.getProject());
+      ProblemDescriptor descriptor =
+          inspectionManager.createProblemDescriptor(
+              target,
+              annotation.getMessage(),
+              DeprecatedLoadQuickFix.INSTANCE,
+              ProblemHighlightType.LIKE_DEPRECATED,
+              true);
+      annotation.registerFix(DeprecatedLoadQuickFix.INSTANCE, null, null, descriptor);
+      return;
+    }
+    markError(target, "Invalid load syntax: missing Skylark module.");
+  }
+}
diff --git a/base/src/com/google/idea/blaze/base/lang/buildfile/views/BuildStructureViewElement.java b/base/src/com/google/idea/blaze/base/lang/buildfile/views/BuildStructureViewElement.java
index 75768dd..7ae6217 100644
--- a/base/src/com/google/idea/blaze/base/lang/buildfile/views/BuildStructureViewElement.java
+++ b/base/src/com/google/idea/blaze/base/lang/buildfile/views/BuildStructureViewElement.java
@@ -21,8 +21,8 @@
 import com.intellij.ide.structureView.StructureViewTreeElement;
 import com.intellij.ide.structureView.impl.common.PsiTreeElementBase;
 import java.util.Collection;
+import javax.annotation.Nullable;
 import org.jetbrains.annotations.NotNull;
-import org.jetbrains.annotations.Nullable;
 
 /** Handles nodes in Structure View. */
 public class BuildStructureViewElement extends PsiTreeElementBase<BuildElement> {
diff --git a/base/src/com/google/idea/blaze/base/lang/projectview/completion/AdditionalLanguagesCompletionContributor.java b/base/src/com/google/idea/blaze/base/lang/projectview/completion/AdditionalLanguagesCompletionContributor.java
index 4cf2f36..598372f 100644
--- a/base/src/com/google/idea/blaze/base/lang/projectview/completion/AdditionalLanguagesCompletionContributor.java
+++ b/base/src/com/google/idea/blaze/base/lang/projectview/completion/AdditionalLanguagesCompletionContributor.java
@@ -17,10 +17,14 @@
 
 import static com.intellij.patterns.PlatformPatterns.psiElement;
 
+import com.google.common.collect.Ordering;
 import com.google.idea.blaze.base.lang.projectview.language.ProjectViewLanguage;
 import com.google.idea.blaze.base.lang.projectview.psi.ProjectViewPsiListSection;
 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.section.sections.AdditionalLanguagesSection;
+import com.google.idea.blaze.base.sync.SyncCache;
+import com.google.idea.blaze.base.sync.projectview.LanguageSupport;
 import com.intellij.codeInsight.completion.AutoCompletionContext;
 import com.intellij.codeInsight.completion.AutoCompletionDecision;
 import com.intellij.codeInsight.completion.CompletionContributor;
@@ -30,8 +34,11 @@
 import com.intellij.codeInsight.completion.CompletionType;
 import com.intellij.codeInsight.lookup.LookupElement;
 import com.intellij.codeInsight.lookup.LookupElementBuilder;
+import com.intellij.openapi.project.Project;
 import com.intellij.patterns.StandardPatterns;
 import com.intellij.util.ProcessingContext;
+import java.util.List;
+import java.util.stream.Collectors;
 
 /** Code completion for additional language types. */
 public class AdditionalLanguagesCompletionContributor extends CompletionContributor {
@@ -62,10 +69,28 @@
               CompletionParameters parameters,
               ProcessingContext context,
               CompletionResultSet result) {
-            for (LanguageClass type : LanguageClass.values()) {
+            for (LanguageClass type :
+                availableAdditionalLanguages(parameters.getEditor().getProject())) {
               result.addElement(LookupElementBuilder.create(type.getName()));
             }
           }
         });
   }
+
+  private static List<LanguageClass> availableAdditionalLanguages(Project project) {
+    List<LanguageClass> langs =
+        SyncCache.getInstance(project)
+            .get(
+                AdditionalLanguagesCompletionContributor.class,
+                (proj, projectData) ->
+                    additionalLanguages(projectData.workspaceLanguageSettings.getWorkspaceType()));
+    return langs == null ? additionalLanguages(LanguageSupport.getDefaultWorkspaceType()) : langs;
+  }
+
+  private static List<LanguageClass> additionalLanguages(WorkspaceType workspaceType) {
+    return LanguageSupport.availableAdditionalLanguages(workspaceType)
+        .stream()
+        .sorted(Ordering.usingToString())
+        .collect(Collectors.toList());
+  }
 }
diff --git a/base/src/com/google/idea/blaze/base/lang/projectview/completion/ProjectViewKeywordCompletionContributor.java b/base/src/com/google/idea/blaze/base/lang/projectview/completion/ProjectViewKeywordCompletionContributor.java
index 88d8d2b..6020f31 100644
--- a/base/src/com/google/idea/blaze/base/lang/projectview/completion/ProjectViewKeywordCompletionContributor.java
+++ b/base/src/com/google/idea/blaze/base/lang/projectview/completion/ProjectViewKeywordCompletionContributor.java
@@ -43,7 +43,7 @@
 import com.intellij.psi.PsiFile;
 import com.intellij.util.ProcessingContext;
 import java.util.List;
-import org.jetbrains.annotations.Nullable;
+import javax.annotation.Nullable;
 
 /** Completes project view section names. */
 public class ProjectViewKeywordCompletionContributor extends CompletionContributor {
diff --git a/base/src/com/google/idea/blaze/base/lang/projectview/documentation/ProjectViewDocumentationProvider.java b/base/src/com/google/idea/blaze/base/lang/projectview/documentation/ProjectViewDocumentationProvider.java
new file mode 100644
index 0000000..fda1879
--- /dev/null
+++ b/base/src/com/google/idea/blaze/base/lang/projectview/documentation/ProjectViewDocumentationProvider.java
@@ -0,0 +1,141 @@
+/*
+ * Copyright 2017 The Bazel Authors. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.idea.blaze.base.lang.projectview.documentation;
+
+import com.google.idea.blaze.base.lang.buildfile.psi.util.PsiUtils;
+import com.google.idea.blaze.base.lang.projectview.psi.ProjectViewSection;
+import com.google.idea.blaze.base.projectview.section.SectionParser;
+import com.google.idea.blaze.base.settings.Blaze;
+import com.intellij.lang.documentation.AbstractDocumentationProvider;
+import com.intellij.lang.documentation.ExternalDocumentationProvider;
+import com.intellij.openapi.editor.Editor;
+import com.intellij.openapi.project.Project;
+import com.intellij.psi.PsiElement;
+import com.intellij.psi.PsiFile;
+import java.io.IOException;
+import java.util.Collections;
+import java.util.List;
+import javax.annotation.Nullable;
+import org.apache.commons.httpclient.HttpClient;
+import org.apache.commons.httpclient.methods.HeadMethod;
+import org.apache.commons.httpclient.params.HttpConnectionManagerParams;
+
+/** Provides quick docs for some .blazeproject elements. */
+public class ProjectViewDocumentationProvider extends AbstractDocumentationProvider
+    implements ExternalDocumentationProvider {
+
+  @Nullable
+  private static SectionParser getSection(PsiElement element) {
+    ProjectViewSection section = PsiUtils.getParentOfType(element, ProjectViewSection.class, false);
+    return section != null ? section.getSectionParser() : null;
+  }
+
+  @Nullable
+  @Override
+  public String generateDoc(PsiElement element, @Nullable PsiElement originalElement) {
+    SectionParser section = getSection(element);
+    if (section == null) {
+      return null;
+    }
+    StringBuilder builder = new StringBuilder();
+    String quickDocs = section.quickDocs();
+    if (quickDocs != null) {
+      builder.append(wrapInTag("<p>" + section.quickDocs(), "code"));
+    }
+    String url = getUrlFor(element.getProject(), section, false);
+    if (url != null) {
+      builder.append(
+          String.format("<p><b>External documentation</b>:<br><a href=\"%s\">%s</a>", url, url));
+    }
+    return wrapInTag(wrapInTag(builder.toString(), "body"), "html");
+  }
+
+  private static String wrapInTag(String doc, String htmlTag) {
+    return String.format("<%s>%s</%s>", htmlTag, doc, htmlTag);
+  }
+
+  @Override
+  public boolean hasDocumentationFor(PsiElement element, PsiElement originalElement) {
+    return getUrlFor(element, false) != null;
+  }
+
+  @Override
+  public List<String> getUrlFor(PsiElement element, PsiElement originalElement) {
+    final String url = getUrlFor(element, true);
+    return url == null ? null : Collections.singletonList(url);
+  }
+
+  @Nullable
+  @Override
+  public String fetchExternalDocumentation(
+      Project project, PsiElement element, List<String> docUrls) {
+    return null;
+  }
+
+  @Override
+  public boolean canPromptToConfigureDocumentation(PsiElement element) {
+    return false;
+  }
+
+  @Override
+  public void promptToConfigureDocumentation(PsiElement element) {}
+
+  @Nullable
+  private static String getUrlFor(PsiElement element, boolean checkExistence) {
+    SectionParser section = getSection(element);
+    return section != null ? getUrlFor(element.getProject(), section, checkExistence) : null;
+  }
+
+  @Nullable
+  private static String getUrlFor(Project project, SectionParser section, boolean checkExistence) {
+    String baseDocsUrl = Blaze.getBuildSystemProvider(project).getProjectViewDocumentationUrl();
+    if (baseDocsUrl == null) {
+      return null;
+    }
+    String url = baseDocsUrl + "#" + section.getName();
+    if (checkExistence && !pageExists(url)) {
+      return baseDocsUrl;
+    }
+    return url;
+  }
+
+  private static boolean pageExists(String url) {
+    final HttpClient client = new HttpClient();
+    final HttpConnectionManagerParams params = client.getHttpConnectionManager().getParams();
+    params.setSoTimeout(5 * 1000);
+    params.setConnectionTimeout(5 * 1000);
+
+    try {
+      final HeadMethod method = new HeadMethod(url);
+      final int rc = client.executeMethod(method);
+      if (rc == 404) {
+        return false;
+      }
+    } catch (IllegalArgumentException e) {
+      return false;
+    } catch (IOException e) {
+      // ignore
+    }
+    return true;
+  }
+
+  @Nullable
+  @Override
+  public PsiElement getCustomDocumentationElement(
+      Editor editor, PsiFile file, @Nullable PsiElement contextElement) {
+    return PsiUtils.getParentOfType(contextElement, ProjectViewSection.class, false);
+  }
+}
diff --git a/base/src/com/google/idea/blaze/base/lang/projectview/formatting/ProjectViewCodeStyleSettingsProvider.java b/base/src/com/google/idea/blaze/base/lang/projectview/formatting/ProjectViewCodeStyleSettingsProvider.java
new file mode 100644
index 0000000..3df7613
--- /dev/null
+++ b/base/src/com/google/idea/blaze/base/lang/projectview/formatting/ProjectViewCodeStyleSettingsProvider.java
@@ -0,0 +1,50 @@
+/*
+ * Copyright 2017 The Bazel Authors. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.idea.blaze.base.lang.projectview.formatting;
+
+import com.google.idea.blaze.base.lang.projectview.language.ProjectViewLanguage;
+import com.intellij.lang.Language;
+import com.intellij.psi.codeStyle.CommonCodeStyleSettings;
+import com.intellij.psi.codeStyle.LanguageCodeStyleSettingsProvider;
+import javax.annotation.Nullable;
+
+/** Allows blazeproject-specific code style settings */
+public class ProjectViewCodeStyleSettingsProvider extends LanguageCodeStyleSettingsProvider {
+
+  @Override
+  public Language getLanguage() {
+    return ProjectViewLanguage.INSTANCE;
+  }
+
+  @Override
+  public String getCodeSample(SettingsType settingsType) {
+    return "";
+  }
+
+  @Nullable
+  @Override
+  public CommonCodeStyleSettings getDefaultCommonSettings() {
+    CommonCodeStyleSettings defaultSettings =
+        new CommonCodeStyleSettings(ProjectViewLanguage.INSTANCE);
+    defaultSettings.LINE_COMMENT_AT_FIRST_COLUMN = false;
+    defaultSettings.LINE_COMMENT_ADD_SPACE = true;
+    CommonCodeStyleSettings.IndentOptions indentOptions = defaultSettings.initIndentOptions();
+    indentOptions.TAB_SIZE = 2;
+    indentOptions.INDENT_SIZE = 2;
+    indentOptions.CONTINUATION_INDENT_SIZE = 2;
+    return defaultSettings;
+  }
+}
diff --git a/base/src/com/google/idea/blaze/base/lang/projectview/formatting/ProjectViewCommenter.java b/base/src/com/google/idea/blaze/base/lang/projectview/formatting/ProjectViewCommenter.java
index 09aaa68..33667ee 100644
--- a/base/src/com/google/idea/blaze/base/lang/projectview/formatting/ProjectViewCommenter.java
+++ b/base/src/com/google/idea/blaze/base/lang/projectview/formatting/ProjectViewCommenter.java
@@ -19,7 +19,7 @@
 import com.intellij.lang.CodeDocumentationAwareCommenter;
 import com.intellij.psi.PsiComment;
 import com.intellij.psi.tree.IElementType;
-import org.jetbrains.annotations.Nullable;
+import javax.annotation.Nullable;
 
 /** Supports (un)commenting lines via IntelliJ */
 public class ProjectViewCommenter implements CodeDocumentationAwareCommenter {
diff --git a/base/src/com/google/idea/blaze/base/lang/projectview/psi/ProjectViewPsiListSection.java b/base/src/com/google/idea/blaze/base/lang/projectview/psi/ProjectViewPsiListSection.java
index d75547a..3d2ad2b 100644
--- a/base/src/com/google/idea/blaze/base/lang/projectview/psi/ProjectViewPsiListSection.java
+++ b/base/src/com/google/idea/blaze/base/lang/projectview/psi/ProjectViewPsiListSection.java
@@ -15,12 +15,22 @@
  */
 package com.google.idea.blaze.base.lang.projectview.psi;
 
+import com.google.idea.blaze.base.lang.projectview.lexer.ProjectViewTokenType;
 import com.intellij.lang.ASTNode;
+import com.intellij.psi.PsiElement;
+import javax.annotation.Nullable;
 
 /** Psi element for list section. */
-public class ProjectViewPsiListSection extends ProjectViewPsiElement {
+public class ProjectViewPsiListSection extends ProjectViewSection {
 
   public ProjectViewPsiListSection(ASTNode node) {
     super(node);
   }
+
+  @Nullable
+  @Override
+  public String getSectionName() {
+    PsiElement keyword = findChildByType(ProjectViewTokenType.LIST_KEYWORD);
+    return keyword != null ? keyword.getText() : null;
+  }
 }
diff --git a/base/src/com/google/idea/blaze/base/lang/projectview/psi/ProjectViewPsiScalarSection.java b/base/src/com/google/idea/blaze/base/lang/projectview/psi/ProjectViewPsiScalarSection.java
index 6f03ff4..19819a4 100644
--- a/base/src/com/google/idea/blaze/base/lang/projectview/psi/ProjectViewPsiScalarSection.java
+++ b/base/src/com/google/idea/blaze/base/lang/projectview/psi/ProjectViewPsiScalarSection.java
@@ -15,12 +15,22 @@
  */
 package com.google.idea.blaze.base.lang.projectview.psi;
 
+import com.google.idea.blaze.base.lang.projectview.lexer.ProjectViewTokenType;
 import com.intellij.lang.ASTNode;
+import com.intellij.psi.PsiElement;
+import javax.annotation.Nullable;
 
 /** Psi element for scalar section. */
-public class ProjectViewPsiScalarSection extends ProjectViewPsiElement {
+public class ProjectViewPsiScalarSection extends ProjectViewSection {
 
   public ProjectViewPsiScalarSection(ASTNode node) {
     super(node);
   }
+
+  @Nullable
+  @Override
+  public String getSectionName() {
+    PsiElement keyword = findChildByType(ProjectViewTokenType.SCALAR_KEYWORD);
+    return keyword != null ? keyword.getText() : null;
+  }
 }
diff --git a/base/src/com/google/idea/blaze/base/lang/projectview/psi/ProjectViewSection.java b/base/src/com/google/idea/blaze/base/lang/projectview/psi/ProjectViewSection.java
new file mode 100644
index 0000000..9e275bf
--- /dev/null
+++ b/base/src/com/google/idea/blaze/base/lang/projectview/psi/ProjectViewSection.java
@@ -0,0 +1,75 @@
+/*
+ * Copyright 2017 The Bazel Authors. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.idea.blaze.base.lang.projectview.psi;
+
+import com.google.idea.blaze.base.projectview.section.SectionParser;
+import com.google.idea.blaze.base.projectview.section.sections.Sections;
+import com.intellij.lang.ASTNode;
+import com.intellij.navigation.ItemPresentation;
+import com.intellij.navigation.NavigationItem;
+import javax.annotation.Nullable;
+import javax.swing.Icon;
+
+/** Psi element for a list or scalar section. */
+public abstract class ProjectViewSection extends ProjectViewPsiElement implements NavigationItem {
+
+  public ProjectViewSection(ASTNode node) {
+    super(node);
+  }
+
+  @Override
+  public ItemPresentation getPresentation() {
+    final ProjectViewSection element = this;
+    return new ItemPresentation() {
+      @Override
+      public String getPresentableText() {
+        return getSectionName();
+      }
+
+      @Override
+      public String getLocationString() {
+        return null;
+      }
+
+      @Override
+      public Icon getIcon(boolean unused) {
+        return element.getIcon(0);
+      }
+    };
+  }
+
+  @Override
+  public String getName() {
+    return getSectionName();
+  }
+
+  @Nullable
+  protected abstract String getSectionName();
+
+  @Nullable
+  public SectionParser getSectionParser() {
+    String text = getSectionName();
+    if (text == null) {
+      return null;
+    }
+    for (SectionParser parser : Sections.getParsers()) {
+      if (text.equals(parser.getName())) {
+        return parser;
+      }
+    }
+    return null;
+  }
+}
diff --git a/base/src/com/google/idea/blaze/base/lang/projectview/stubs/ProjectViewFileStubBuilder.java b/base/src/com/google/idea/blaze/base/lang/projectview/stubs/ProjectViewFileStubBuilder.java
new file mode 100644
index 0000000..dcf8917
--- /dev/null
+++ b/base/src/com/google/idea/blaze/base/lang/projectview/stubs/ProjectViewFileStubBuilder.java
@@ -0,0 +1,43 @@
+/*
+ * Copyright 2017 The Bazel Authors. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.idea.blaze.base.lang.projectview.stubs;
+
+import com.intellij.openapi.vfs.VirtualFile;
+import com.intellij.psi.stubs.BinaryFileStubBuilder;
+import com.intellij.psi.stubs.Stub;
+import com.intellij.util.indexing.FileContent;
+import javax.annotation.Nullable;
+
+/** Empty stub builder to suppress errors when IntelliJ is looking for stubs. */
+public class ProjectViewFileStubBuilder implements BinaryFileStubBuilder {
+  private static final int STUB_VERSION = 0;
+
+  @Override
+  public boolean acceptsFile(VirtualFile file) {
+    return false;
+  }
+
+  @Nullable
+  @Override
+  public Stub buildStubTree(FileContent fileContent) {
+    return null;
+  }
+
+  @Override
+  public int getStubVersion() {
+    return STUB_VERSION;
+  }
+}
diff --git a/base/src/com/google/idea/blaze/base/logging/EventLogger.java b/base/src/com/google/idea/blaze/base/logging/EventLogger.java
new file mode 100644
index 0000000..02a930f
--- /dev/null
+++ b/base/src/com/google/idea/blaze/base/logging/EventLogger.java
@@ -0,0 +1,50 @@
+/*
+ * Copyright 2017 The Bazel Authors. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.idea.blaze.base.logging;
+
+import com.intellij.openapi.extensions.ExtensionPointName;
+import java.util.Map;
+import org.jetbrains.annotations.Nullable;
+
+/**
+ * Forwards the event logs to an applicable receiver extension or discard them if no applicable
+ * receivers exist.
+ */
+public interface EventLogger {
+  ExtensionPointName<EventLogger> EP_NAME =
+      new ExtensionPointName<>("com.google.idea.blaze.EventLogger");
+
+  static EventLogger getInstance() {
+    for (EventLogger logger : EP_NAME.getExtensions()) {
+      if (logger.isApplicable()) {
+        return logger;
+      }
+    }
+    return NullEventLogger.SINGLETON;
+  }
+
+  boolean isApplicable();
+
+  default void log(Class<?> loggingClass, String eventType, Map<String, String> keyValues) {
+    log(loggingClass, eventType, keyValues, null);
+  }
+
+  void log(
+      Class<?> loggingClass,
+      String eventType,
+      Map<String, String> keyValues,
+      @Nullable Long durationInNanos);
+}
diff --git a/base/src/com/google/idea/blaze/base/logging/NullEventLogger.java b/base/src/com/google/idea/blaze/base/logging/NullEventLogger.java
new file mode 100644
index 0000000..0546f7b
--- /dev/null
+++ b/base/src/com/google/idea/blaze/base/logging/NullEventLogger.java
@@ -0,0 +1,38 @@
+/*
+ * Copyright 2017 The Bazel Authors. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.idea.blaze.base.logging;
+
+import java.util.Map;
+import org.jetbrains.annotations.Nullable;
+
+/** No-op logger used when no logger is not available to receive logs. */
+public class NullEventLogger implements EventLogger {
+  static final NullEventLogger SINGLETON = new NullEventLogger();
+
+  private NullEventLogger() {}
+
+  @Override
+  public boolean isApplicable() {
+    return true;
+  }
+
+  @Override
+  public void log(
+      Class<?> loggingClass,
+      String eventType,
+      Map<String, String> keyValues,
+      @Nullable Long durationInNanos) {}
+}
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 77862fd..bba404b 100644
--- a/base/src/com/google/idea/blaze/base/model/BlazeProjectData.java
+++ b/base/src/com/google/idea/blaze/base/model/BlazeProjectData.java
@@ -15,13 +15,12 @@
  */
 package com.google.idea.blaze.base.model;
 
-import com.google.common.collect.ImmutableMap;
 import com.google.common.collect.ImmutableMultimap;
+import com.google.idea.blaze.base.command.info.BlazeInfo;
 import com.google.idea.blaze.base.ideinfo.TargetKey;
 import com.google.idea.blaze.base.ideinfo.TargetMap;
 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.WorkspacePathResolver;
 import java.io.Serializable;
 import javax.annotation.concurrent.Immutable;
@@ -29,12 +28,11 @@
 /** The top-level object serialized to cache. */
 @Immutable
 public class BlazeProjectData implements Serializable {
-  private static final long serialVersionUID = 27L;
+  private static final long serialVersionUID = 28L;
 
   public final long syncTime;
   public final TargetMap targetMap;
-  public final ImmutableMap<String, String> blazeInfo;
-  public final BlazeRoots blazeRoots;
+  public final BlazeInfo blazeInfo;
   public final BlazeVersionData blazeVersionData;
   public final WorkspacePathResolver workspacePathResolver;
   public final ArtifactLocationDecoder artifactLocationDecoder;
@@ -45,19 +43,16 @@
   public BlazeProjectData(
       long syncTime,
       TargetMap targetMap,
-      ImmutableMap<String, String> blazeInfo,
-      BlazeRoots blazeRoots,
+      BlazeInfo blazeInfo,
       BlazeVersionData blazeVersionData,
       WorkspacePathResolver workspacePathResolver,
       ArtifactLocationDecoder artifactLocationDecoder,
       WorkspaceLanguageSettings workspaceLanguageSettings,
       SyncState syncState,
-      ImmutableMultimap<TargetKey, TargetKey> reverseDependencies,
-      Object dummy) {
+      ImmutableMultimap<TargetKey, TargetKey> reverseDependencies) {
     this.syncTime = syncTime;
     this.targetMap = targetMap;
     this.blazeInfo = blazeInfo;
-    this.blazeRoots = blazeRoots;
     this.blazeVersionData = blazeVersionData;
     this.workspacePathResolver = workspacePathResolver;
     this.artifactLocationDecoder = artifactLocationDecoder;
diff --git a/base/src/com/google/idea/blaze/base/model/BlazeVersionData.java b/base/src/com/google/idea/blaze/base/model/BlazeVersionData.java
index 1cbdbff..ded37ad 100644
--- a/base/src/com/google/idea/blaze/base/model/BlazeVersionData.java
+++ b/base/src/com/google/idea/blaze/base/model/BlazeVersionData.java
@@ -16,9 +16,9 @@
 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.command.info.BlazeInfo;
 import com.google.idea.blaze.base.model.primitives.WorkspaceRoot;
 import com.google.idea.blaze.base.settings.Blaze.BuildSystem;
 import java.io.Serializable;
@@ -59,10 +59,12 @@
     return bazelVersion != null && bazelVersion.isAtLeast(major, minor, bugfix);
   }
 
+  public BuildSystem buildSystem() {
+    return bazelVersion != null ? BuildSystem.Bazel : BuildSystem.Blaze;
+  }
+
   public static BlazeVersionData build(
-      BuildSystem buildSystem,
-      WorkspaceRoot workspaceRoot,
-      ImmutableMap<String, String> blazeInfo) {
+      BuildSystem buildSystem, WorkspaceRoot workspaceRoot, BlazeInfo blazeInfo) {
     Builder builder = new Builder();
     for (BuildSystemProvider provider : BuildSystemProvider.EP_NAME.getExtensions()) {
       provider.populateBlazeVersionData(buildSystem, workspaceRoot, blazeInfo, builder);
diff --git a/base/src/com/google/idea/blaze/base/model/primitives/ExecutionRootPath.java b/base/src/com/google/idea/blaze/base/model/primitives/ExecutionRootPath.java
index 4c8d3f0..5e060b7 100644
--- a/base/src/com/google/idea/blaze/base/model/primitives/ExecutionRootPath.java
+++ b/base/src/com/google/idea/blaze/base/model/primitives/ExecutionRootPath.java
@@ -87,7 +87,7 @@
     if (root.isAbsolute() != path.isAbsolute()) {
       return null;
     }
-    if (!isAncestor(root.getPath(), path.getPath(), false /* strict */)) {
+    if (!isAncestor(root.getPath(), path.getPath(), /* strict */ false)) {
       return null;
     }
     String relativePath = FileUtil.getRelativePath(root, path);
diff --git a/base/src/com/google/idea/blaze/base/model/primitives/Kind.java b/base/src/com/google/idea/blaze/base/model/primitives/Kind.java
index eba71f9..b5fbda8 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
@@ -15,9 +15,11 @@
  */
 package com.google.idea.blaze.base.model.primitives;
 
+import com.google.common.collect.ImmutableCollection;
 import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.ImmutableMultimap;
 import java.util.Arrays;
-import java.util.List;
+import java.util.Collection;
 
 /** Wrapper around a string for a blaze kind (android_library, android_test...) */
 public enum Kind {
@@ -25,6 +27,7 @@
   ANDROID_LIBRARY("android_library", LanguageClass.ANDROID),
   ANDROID_TEST("android_test", LanguageClass.ANDROID),
   ANDROID_ROBOLECTRIC_TEST("android_robolectric_test", LanguageClass.ANDROID),
+  ANDROID_SDK("android_sdk", LanguageClass.ANDROID),
   JAVA_LIBRARY("java_library", LanguageClass.JAVA),
   JAVA_TEST("java_test", LanguageClass.JAVA),
   JAVA_BINARY("java_binary", LanguageClass.JAVA),
@@ -57,10 +60,16 @@
   GO_APPENGINE_LIBRARY("go_appengine_library", LanguageClass.GO),
   GO_WRAP_CC("go_wrap_cc", LanguageClass.GO),
   INTELLIJ_PLUGIN_DEBUG_TARGET("intellij_plugin_debug_target", LanguageClass.JAVA),
+  SCALA_BINARY("scala_binary", LanguageClass.SCALA),
+  SCALA_LIBRARY("scala_library", LanguageClass.SCALA),
+  SCALA_MACRO_LIBRARY("scala_macro_library", LanguageClass.SCALA),
+  SCALA_TEST("scala_test", LanguageClass.SCALA),
   ;
 
   static final ImmutableMap<String, Kind> STRING_TO_KIND = makeStringToKindMap();
 
+  static final ImmutableMultimap<LanguageClass, Kind> PER_LANGUAGES_KINDS = makePerLanguageMap();
+
   private static ImmutableMap<String, Kind> makeStringToKindMap() {
     ImmutableMap.Builder<String, Kind> result = ImmutableMap.builder();
     for (Kind kind : Kind.values()) {
@@ -69,10 +78,22 @@
     return result.build();
   }
 
+  private static ImmutableMultimap<LanguageClass, Kind> makePerLanguageMap() {
+    ImmutableMultimap.Builder<LanguageClass, Kind> result = ImmutableMultimap.builder();
+    for (Kind kind : Kind.values()) {
+      result.put(kind.languageClass, kind);
+    }
+    return result.build();
+  }
+
   public static Kind fromString(String kindString) {
     return STRING_TO_KIND.get(kindString);
   }
 
+  public static ImmutableCollection<Kind> allKindsForLanguage(LanguageClass language) {
+    return PER_LANGUAGES_KINDS.get(language);
+  }
+
   private final String kind;
   private final LanguageClass languageClass;
 
@@ -94,7 +115,7 @@
     return isOneOf(Arrays.asList(kinds));
   }
 
-  public boolean isOneOf(List<Kind> kinds) {
+  public boolean isOneOf(Collection<Kind> kinds) {
     for (Kind kind : kinds) {
       if (this.equals(kind)) {
         return true;
diff --git a/base/src/com/google/idea/blaze/base/model/primitives/Label.java b/base/src/com/google/idea/blaze/base/model/primitives/Label.java
index c06f6a2..f86da52 100644
--- a/base/src/com/google/idea/blaze/base/model/primitives/Label.java
+++ b/base/src/com/google/idea/blaze/base/model/primitives/Label.java
@@ -23,7 +23,7 @@
 import javax.annotation.Nullable;
 import javax.annotation.concurrent.Immutable;
 
-/** Wrapper around a string for a blaze label (//package:rule). */
+/** Wrapper around a string for a blaze label ([@external_workspace]//package:rule). */
 @Immutable
 public final class Label extends TargetExpression {
   private static final Logger logger = Logger.getInstance(Label.class);
@@ -39,16 +39,31 @@
     return null;
   }
 
-  public Label(String label) {
-    super(label);
+  public static Label create(String label) {
     List<BlazeValidationError> errors = Lists.newArrayList();
     if (!validate(label, errors)) {
       BlazeValidationError.throwError(errors);
     }
+    return new Label(label);
   }
 
-  public Label(WorkspacePath packageName, TargetName newTargetName) {
-    this("//" + packageName.toString() + ":" + newTargetName.toString());
+  public static Label create(WorkspacePath packageName, TargetName newTargetName) {
+    return create(null, packageName, newTargetName);
+  }
+
+  public static Label create(
+      @Nullable String externalWorkspaceName, WorkspacePath packagePath, TargetName targetName) {
+    String fullLabel =
+        String.format(
+            "%s//%s:%s",
+            externalWorkspaceName != null ? "@" + externalWorkspaceName : "",
+            packagePath,
+            targetName);
+    return new Label(fullLabel);
+  }
+
+  private Label(String label) {
+    super(label);
   }
 
   public static boolean validate(String label) {
@@ -81,6 +96,25 @@
     return false;
   }
 
+  public boolean isExternal() {
+    return toString().startsWith("@");
+  }
+
+  /**
+   * Returns the external workspace referenced by this label, or null if it's a main workspace
+   * label.
+   */
+  @Nullable
+  public String externalWorkspaceName() {
+    String label = toString();
+    if (!label.startsWith("@")) {
+      return null;
+    }
+    int slashesIndex = label.indexOf("//");
+    logger.assertTrue(slashesIndex >= 0);
+    return label.substring(1, slashesIndex);
+  }
+
   /**
    * Extract the target name from a label. The target name follows a colon at the end of the label.
    *
@@ -106,7 +140,17 @@
     return new WorkspacePath(labelStr.substring(startIndex, colonIndex));
   }
 
-  public static boolean validatePackagePath(String path) {
+  /** A new label with the same workspace and package paths, but a different target name. */
+  @Nullable
+  public Label withTargetName(@Nullable String targetName) {
+    if (targetName == null) {
+      return null;
+    }
+    TargetName target = TargetName.createIfValid(targetName);
+    return target != null ? Label.create(externalWorkspaceName(), blazePackage(), target) : null;
+  }
+
+  static boolean validatePackagePath(String path) {
     return validatePackagePath(path, null);
   }
 
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 cf030a2..e9cb3a8 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
@@ -29,7 +29,9 @@
   TYPESCRIPT("typescript", ImmutableSet.of("ts", "ats")),
   DART("dart", ImmutableSet.of("dart")),
   GO("go", ImmutableSet.of("go")),
-  PYTHON("python", ImmutableSet.of("py", "pyw"));
+  PYTHON("python", ImmutableSet.of("py", "pyw")),
+  SCALA("scala", ImmutableSet.of("scala")),
+  ;
 
   private static final ImmutableMap<String, LanguageClass> RECOGNIZED_EXTENSIONS =
       extensionToClassMap();
@@ -65,6 +67,11 @@
     return null;
   }
 
+  @Override
+  public String toString() {
+    return name;
+  }
+
   /** Returns the LanguageClass associated with the given filename extension, if it's recognized. */
   @Nullable
   public static LanguageClass fromExtension(String filenameExtension) {
diff --git a/base/src/com/google/idea/blaze/base/model/primitives/TargetExpression.java b/base/src/com/google/idea/blaze/base/model/primitives/TargetExpression.java
index ea3d847..4e36aab 100644
--- a/base/src/com/google/idea/blaze/base/model/primitives/TargetExpression.java
+++ b/base/src/com/google/idea/blaze/base/model/primitives/TargetExpression.java
@@ -32,16 +32,21 @@
    *     it is not.
    */
   public static TargetExpression fromString(String expression) {
-    return Label.validate(expression) ? new Label(expression) : new TargetExpression(expression);
+    return Label.validate(expression) ? Label.create(expression) : new TargetExpression(expression);
   }
 
-  TargetExpression(String expression) {
+  protected TargetExpression(String expression) {
     // TODO(joshgiles): Validation/canonicalization for target expressions.
     // For reference, handled in Blaze/Bazel in TargetPattern.java.
     Preconditions.checkArgument(!expression.isEmpty(), "Target should be non-empty.");
     this.expression = expression;
   }
 
+  /** Is this an excluded target expression (i.e. starts with '-')? */
+  public boolean isExcluded() {
+    return expression.startsWith("-");
+  }
+
   @Override
   public String toString() {
     return expression;
@@ -63,8 +68,7 @@
 
   /** All targets in all packages below the given path */
   public static TargetExpression allFromPackageRecursive(WorkspacePath localPackage) {
-    if (localPackage.relativePath().isEmpty()) {
-      // localPackage is the workspace root
+    if (localPackage.isWorkspaceRoot()) {
       return new TargetExpression("//...:all");
     }
     return new TargetExpression("//" + localPackage.relativePath() + "/...:all");
diff --git a/base/src/com/google/idea/blaze/base/model/primitives/WorkspacePath.java b/base/src/com/google/idea/blaze/base/model/primitives/WorkspacePath.java
index 00ce9ee..b3cbbf0 100644
--- a/base/src/com/google/idea/blaze/base/model/primitives/WorkspacePath.java
+++ b/base/src/com/google/idea/blaze/base/model/primitives/WorkspacePath.java
@@ -18,9 +18,9 @@
 import com.google.idea.blaze.base.ui.BlazeValidationError;
 import java.io.Serializable;
 import java.util.Collection;
+import javax.annotation.Nullable;
 import javax.annotation.concurrent.Immutable;
 import org.jetbrains.annotations.NotNull;
-import org.jetbrains.annotations.Nullable;
 
 /**
  * Represents a path relative to the workspace root. The path component separator is Blaze specific.
diff --git a/base/src/com/google/idea/blaze/base/model/primitives/WorkspaceRoot.java b/base/src/com/google/idea/blaze/base/model/primitives/WorkspaceRoot.java
index b4a0a13..53d156d 100644
--- a/base/src/com/google/idea/blaze/base/model/primitives/WorkspaceRoot.java
+++ b/base/src/com/google/idea/blaze/base/model/primitives/WorkspaceRoot.java
@@ -108,7 +108,8 @@
 
   private WorkspacePath workspacePathFor(String path) {
     if (!isInWorkspace(path)) {
-      throw new IllegalArgumentException("File is not under this workspace");
+      throw new IllegalArgumentException(
+          String.format("File '%s' is not under workspace %s", path, directory));
     }
     if (directory.getPath().length() == path.length()) {
       return new WorkspacePath("");
diff --git a/base/src/com/google/idea/blaze/base/plugin/BlazeActionRemover.java b/base/src/com/google/idea/blaze/base/plugin/BlazeActionRemover.java
index 17e6043..c59731b 100644
--- a/base/src/com/google/idea/blaze/base/plugin/BlazeActionRemover.java
+++ b/base/src/com/google/idea/blaze/base/plugin/BlazeActionRemover.java
@@ -32,7 +32,7 @@
     }
   }
 
-  private static void replaceAction(String actionId, AnAction newAction) {
+  public static void replaceAction(String actionId, AnAction newAction) {
     ActionManager actionManager = ActionManager.getInstance();
     AnAction oldAction = actionManager.getAction(actionId);
     if (oldAction != null) {
diff --git a/base/src/com/google/idea/blaze/base/plugin/PluginUtils.java b/base/src/com/google/idea/blaze/base/plugin/PluginUtils.java
new file mode 100644
index 0000000..6156cbc
--- /dev/null
+++ b/base/src/com/google/idea/blaze/base/plugin/PluginUtils.java
@@ -0,0 +1,63 @@
+/*
+ * Copyright 2017 The Bazel Authors. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.idea.blaze.base.plugin;
+
+import com.google.common.collect.ImmutableSet;
+import com.intellij.ide.plugins.IdeaPluginDescriptor;
+import com.intellij.ide.plugins.PluginManager;
+import com.intellij.openapi.extensions.PluginId;
+import com.intellij.openapi.updateSettings.impl.pluginsAdvertisement.PluginsAdvertiser;
+import com.intellij.openapi.util.EmptyRunnable;
+import com.intellij.pom.Navigatable;
+import com.intellij.pom.NavigatableAdapter;
+
+/** Utility methods for querying / manipulating other plugins. */
+public final class PluginUtils {
+
+  private PluginUtils() {}
+
+  /** If the plugin is already installed, enable it, otherwise both install and enable it. */
+  public static void installOrEnablePlugin(String pluginId) {
+    if (isPluginInstalled(pluginId)) {
+      PluginManager.enablePlugin(pluginId);
+    } else {
+      PluginsAdvertiser.installAndEnablePlugins(ImmutableSet.of(pluginId), EmptyRunnable.INSTANCE);
+    }
+  }
+
+  /** Returns a {@link Navigatable} which will install (if necessary) and enable the given plugin */
+  public static Navigatable installOrEnablePluginNavigable(String pluginId) {
+    return new NavigatableAdapter() {
+      @Override
+      public void navigate(boolean requestFocus) {
+        installOrEnablePlugin(pluginId);
+      }
+    };
+  }
+
+  public static boolean isPluginInstalled(String pluginId) {
+    return getPluginDescriptor(pluginId) != null;
+  }
+
+  public static boolean isPluginEnabled(String pluginId) {
+    IdeaPluginDescriptor descriptor = getPluginDescriptor(pluginId);
+    return descriptor != null && descriptor.isEnabled();
+  }
+
+  private static IdeaPluginDescriptor getPluginDescriptor(String pluginId) {
+    return PluginManager.getPlugin(PluginId.getId(pluginId));
+  }
+}
diff --git a/base/src/com/google/idea/blaze/base/prefetch/PrefetchFileSource.java b/base/src/com/google/idea/blaze/base/prefetch/PrefetchFileSource.java
index dcdc666..26c7491 100644
--- a/base/src/com/google/idea/blaze/base/prefetch/PrefetchFileSource.java
+++ b/base/src/com/google/idea/blaze/base/prefetch/PrefetchFileSource.java
@@ -16,6 +16,7 @@
 package com.google.idea.blaze.base.prefetch;
 
 import com.google.idea.blaze.base.model.BlazeProjectData;
+import com.google.idea.blaze.base.projectview.ProjectViewSet;
 import com.intellij.openapi.extensions.ExtensionPointName;
 import com.intellij.openapi.project.Project;
 import java.io.File;
@@ -28,7 +29,10 @@
       ExtensionPointName.create("com.google.idea.blaze.PrefetchFileSource");
   /** Adds any files or directories that we would be interested in prefetching. */
   void addFilesToPrefetch(
-      Project project, BlazeProjectData blazeProjectData, Collection<File> files);
+      Project project,
+      ProjectViewSet projectViewSet,
+      BlazeProjectData blazeProjectData,
+      Collection<File> files);
 
   /** Returns any source file extensions that are a good candidate for the {@link Prefetcher}. */
   Set<String> prefetchSrcFileExtensions();
diff --git a/base/src/com/google/idea/blaze/base/prefetch/PrefetchProjectInitializer.java b/base/src/com/google/idea/blaze/base/prefetch/PrefetchProjectInitializer.java
new file mode 100644
index 0000000..ed7b4cc
--- /dev/null
+++ b/base/src/com/google/idea/blaze/base/prefetch/PrefetchProjectInitializer.java
@@ -0,0 +1,116 @@
+/*
+ * Copyright 2017 The Bazel Authors. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.idea.blaze.base.prefetch;
+
+import com.google.common.util.concurrent.ListenableFuture;
+import com.google.idea.blaze.base.model.BlazeProjectData;
+import com.google.idea.blaze.base.projectview.ProjectViewManager;
+import com.google.idea.blaze.base.projectview.ProjectViewSet;
+import com.google.idea.blaze.base.settings.BlazeImportSettings;
+import com.google.idea.blaze.base.settings.BlazeImportSettingsManager;
+import com.google.idea.blaze.base.sync.data.BlazeProjectDataManagerImpl;
+import com.google.idea.common.experiments.BoolExperiment;
+import com.intellij.openapi.application.TransactionGuard;
+import com.intellij.openapi.components.ApplicationComponent;
+import com.intellij.openapi.diagnostic.Logger;
+import com.intellij.openapi.progress.ProgressIndicator;
+import com.intellij.openapi.project.DumbModeTask;
+import com.intellij.openapi.project.DumbService;
+import com.intellij.openapi.project.Project;
+import com.intellij.openapi.project.ProjectManager;
+import com.intellij.openapi.project.ProjectManagerAdapter;
+import com.intellij.util.TimeoutUtil;
+import java.io.IOException;
+import javax.annotation.Nullable;
+
+/** Run prefetching on project open, prior to initial indexing step. */
+public class PrefetchProjectInitializer extends ApplicationComponent.Adapter {
+
+  private static final Logger logger = Logger.getInstance(PrefetchProjectInitializer.class);
+
+  private static final BoolExperiment prefetchOnProjectOpen =
+      new BoolExperiment("prefetch.on.project.open2", true);
+
+  @Override
+  public void initComponent() {
+    ProjectManager projectManager = ProjectManager.getInstance();
+    projectManager.addProjectManagerListener(
+        new ProjectManagerAdapter() {
+          @Override
+          public void projectOpened(Project project) {
+            if (prefetchOnProjectOpen.getValue()) {
+              prefetchProjectFiles(project);
+            }
+          }
+        });
+  }
+
+  private static void prefetchProjectFiles(Project project) {
+    BlazeProjectData projectData = getBlazeProjectData(project);
+    if (projectData == null) {
+      return;
+    }
+    ProjectViewSet projectViewSet = ProjectViewManager.getInstance(project).getProjectViewSet();
+    if (projectViewSet == null) {
+      return;
+    }
+    long start = System.currentTimeMillis();
+    ListenableFuture<?> future =
+        PrefetchService.getInstance().prefetchProjectFiles(project, projectViewSet, projectData);
+    TransactionGuard.submitTransaction(
+        project,
+        () -> {
+          DumbService.getInstance(project).queueTask(new PrefetchTask(future, start));
+        });
+  }
+
+  static class PrefetchTask extends DumbModeTask {
+    private final ListenableFuture<?> future;
+    private final long startTimeMillis;
+
+    private PrefetchTask(ListenableFuture<?> future, long startTimeMillis) {
+      this.future = future;
+      this.startTimeMillis = startTimeMillis;
+    }
+
+    @Override
+    public void performInDumbMode(ProgressIndicator indicator) {
+      indicator.setIndeterminate(true);
+      indicator.setText("Prefetching files...");
+      while (!future.isCancelled() && !future.isDone()) {
+        indicator.checkCanceled();
+        TimeoutUtil.sleep(100);
+      }
+      long end = System.currentTimeMillis();
+      logger.info(String.format("Initial prefetching took: %d ms", (end - startTimeMillis)));
+    }
+  }
+
+  @Nullable
+  private static BlazeProjectData getBlazeProjectData(Project project) {
+    BlazeImportSettings importSettings =
+        BlazeImportSettingsManager.getInstance(project).getImportSettings();
+    if (importSettings == null) {
+      return null;
+    }
+    try {
+      return BlazeProjectDataManagerImpl.getImpl(project).loadProjectRoot(importSettings);
+    } catch (IOException e) {
+      // ignore: if we can't load the previous project data, we don't know what to prefetch
+      return null;
+    }
+  }
+}
diff --git a/base/src/com/google/idea/blaze/base/prefetch/PrefetchService.java b/base/src/com/google/idea/blaze/base/prefetch/PrefetchService.java
index fc5f5e2..726f970 100644
--- a/base/src/com/google/idea/blaze/base/prefetch/PrefetchService.java
+++ b/base/src/com/google/idea/blaze/base/prefetch/PrefetchService.java
@@ -17,6 +17,7 @@
 
 import com.google.common.util.concurrent.ListenableFuture;
 import com.google.idea.blaze.base.model.BlazeProjectData;
+import com.google.idea.blaze.base.projectview.ProjectViewSet;
 import com.intellij.openapi.components.ServiceManager;
 import com.intellij.openapi.project.Project;
 import java.io.File;
@@ -31,5 +32,6 @@
   /** Instructs all prefetchers to prefetch these files. */
   ListenableFuture<?> prefetchFiles(Project project, Collection<File> files);
 
-  ListenableFuture<?> prefetchProjectFiles(Project project, BlazeProjectData blazeProjectData);
+  ListenableFuture<?> prefetchProjectFiles(
+      Project project, ProjectViewSet projectViewSet, BlazeProjectData blazeProjectData);
 }
diff --git a/base/src/com/google/idea/blaze/base/prefetch/PrefetchServiceImpl.java b/base/src/com/google/idea/blaze/base/prefetch/PrefetchServiceImpl.java
index 8046db0..05e02a6 100644
--- a/base/src/com/google/idea/blaze/base/prefetch/PrefetchServiceImpl.java
+++ b/base/src/com/google/idea/blaze/base/prefetch/PrefetchServiceImpl.java
@@ -22,7 +22,6 @@
 import com.google.idea.blaze.base.model.BlazeProjectData;
 import com.google.idea.blaze.base.model.primitives.WorkspacePath;
 import com.google.idea.blaze.base.model.primitives.WorkspaceRoot;
-import com.google.idea.blaze.base.projectview.ProjectViewManager;
 import com.google.idea.blaze.base.projectview.ProjectViewSet;
 import com.google.idea.blaze.base.settings.BlazeImportSettings;
 import com.google.idea.blaze.base.settings.BlazeImportSettingsManager;
@@ -47,11 +46,7 @@
 
   @Override
   public ListenableFuture<?> prefetchProjectFiles(
-      Project project, BlazeProjectData blazeProjectData) {
-    ProjectViewSet projectViewSet = ProjectViewManager.getInstance(project).getProjectViewSet();
-    if (projectViewSet == null) {
-      return Futures.immediateFuture(null);
-    }
+      Project project, ProjectViewSet projectViewSet, BlazeProjectData blazeProjectData) {
     BlazeImportSettings importSettings =
         BlazeImportSettingsManager.getInstance(project).getImportSettings();
     if (importSettings == null) {
@@ -68,7 +63,7 @@
       files.add(workspaceRoot.fileForPath(workspacePath));
     }
     for (PrefetchFileSource fileSource : PrefetchFileSource.EP_NAME.getExtensions()) {
-      fileSource.addFilesToPrefetch(project, blazeProjectData, files);
+      fileSource.addFilesToPrefetch(project, projectViewSet, blazeProjectData, files);
     }
     return prefetchFiles(project, files);
   }
diff --git a/base/src/com/google/idea/blaze/base/projectview/ProjectView.java b/base/src/com/google/idea/blaze/base/projectview/ProjectView.java
index 8dfc6f3..4d09c18 100644
--- a/base/src/com/google/idea/blaze/base/projectview/ProjectView.java
+++ b/base/src/com/google/idea/blaze/base/projectview/ProjectView.java
@@ -17,11 +17,15 @@
 
 import com.google.common.base.Objects;
 import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Iterables;
 import com.google.common.collect.Lists;
+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.Section;
 import com.google.idea.blaze.base.projectview.section.SectionBuilder;
 import com.google.idea.blaze.base.projectview.section.SectionKey;
 import java.io.Serializable;
+import java.util.Collection;
 import java.util.List;
 import javax.annotation.Nullable;
 
@@ -47,6 +51,32 @@
     return result.build();
   }
 
+  /** Returns all values from the given list section */
+  public <T> List<T> listItems(SectionKey<T, ListSection<T>> key) {
+    List<T> result = Lists.newArrayList();
+    for (ListSection<T> section : getSectionsOfType(key)) {
+      result.addAll(section.items());
+    }
+    return result;
+  }
+
+  /** Gets the last value from any scalar sections */
+  @Nullable
+  public <T> T getScalarValue(SectionKey<T, ScalarSection<T>> key) {
+    return getScalarValue(key, null);
+  }
+
+  /** Gets the last value from any scalar sections */
+  @Nullable
+  public <T> T getScalarValue(SectionKey<T, ScalarSection<T>> key, @Nullable T defaultValue) {
+    Collection<ScalarSection<T>> sections = getSectionsOfType(key);
+    if (sections.isEmpty()) {
+      return defaultValue;
+    } else {
+      return Iterables.getLast(sections).getValue();
+    }
+  }
+
   public static Builder builder() {
     return new Builder();
   }
@@ -107,6 +137,11 @@
       return this;
     }
 
+    public <T> Builder remove(Section<T> section) {
+      sections.remove(section);
+      return this;
+    }
+
     /** Replaces a section if it already exists. If it doesn't, just add the section. */
     public <T, SectionType extends Section<T>> Builder replace(
         @Nullable Section<T> section, SectionBuilder<T, SectionType> builder) {
diff --git a/base/src/com/google/idea/blaze/base/projectview/ProjectViewManagerImpl.java b/base/src/com/google/idea/blaze/base/projectview/ProjectViewManagerImpl.java
index bab57dc..1635b84 100644
--- a/base/src/com/google/idea/blaze/base/projectview/ProjectViewManagerImpl.java
+++ b/base/src/com/google/idea/blaze/base/projectview/ProjectViewManagerImpl.java
@@ -28,8 +28,8 @@
 import java.io.File;
 import java.io.IOException;
 import java.util.List;
+import javax.annotation.Nullable;
 import org.jetbrains.annotations.NotNull;
-import org.jetbrains.annotations.Nullable;
 
 /** Project view manager implementation. */
 /** Stores mutable per-project user settings. */
diff --git a/base/src/com/google/idea/blaze/base/projectview/ProjectViewSet.java b/base/src/com/google/idea/blaze/base/projectview/ProjectViewSet.java
index 1f3ed1f..e2e1211 100644
--- a/base/src/com/google/idea/blaze/base/projectview/ProjectViewSet.java
+++ b/base/src/com/google/idea/blaze/base/projectview/ProjectViewSet.java
@@ -63,7 +63,8 @@
   }
 
   /** Gets the last value from any scalar sections */
-  public <T> T getScalarValue(SectionKey<T, ScalarSection<T>> key, T defaultValue) {
+  @Nullable
+  public <T> T getScalarValue(SectionKey<T, ScalarSection<T>> key, @Nullable T defaultValue) {
     Collection<ScalarSection<T>> sections = getSections(key);
     if (sections.isEmpty()) {
       return defaultValue;
@@ -82,7 +83,7 @@
     return result;
   }
 
-  public Collection<ProjectViewFile> getProjectViewFiles() {
+  public ImmutableList<ProjectViewFile> getProjectViewFiles() {
     return projectViewFiles;
   }
 
diff --git a/base/src/com/google/idea/blaze/base/projectview/ProjectViewStorageManagerImpl.java b/base/src/com/google/idea/blaze/base/projectview/ProjectViewStorageManagerImpl.java
index 729b1d8..49a5d3b 100644
--- a/base/src/com/google/idea/blaze/base/projectview/ProjectViewStorageManagerImpl.java
+++ b/base/src/com/google/idea/blaze/base/projectview/ProjectViewStorageManagerImpl.java
@@ -17,18 +17,16 @@
 
 import com.google.common.base.Charsets;
 import com.google.common.collect.ImmutableList;
-import com.intellij.openapi.diagnostic.Logger;
 import com.intellij.openapi.vfs.LocalFileSystem;
 import java.io.File;
 import java.io.FileInputStream;
 import java.io.FileWriter;
 import java.io.IOException;
+import javax.annotation.Nullable;
 import org.jetbrains.annotations.NotNull;
-import org.jetbrains.annotations.Nullable;
 
 /** Project view storage implementation. */
 final class ProjectViewStorageManagerImpl extends ProjectViewStorageManager {
-  private static final Logger logger = Logger.getInstance(ProjectViewManagerImpl.class);
 
   @Nullable
   @Override
diff --git a/base/src/com/google/idea/blaze/base/projectview/ProjectViewVerifier.java b/base/src/com/google/idea/blaze/base/projectview/ProjectViewVerifier.java
index 9b4b465..1d35932 100644
--- a/base/src/com/google/idea/blaze/base/projectview/ProjectViewVerifier.java
+++ b/base/src/com/google/idea/blaze/base/projectview/ProjectViewVerifier.java
@@ -29,11 +29,14 @@
 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.projectview.LanguageSupport;
 import com.google.idea.blaze.base.sync.projectview.WorkspaceLanguageSettings;
 import com.google.idea.blaze.base.sync.workspace.WorkspacePathResolver;
+import com.intellij.openapi.project.Project;
 import com.intellij.openapi.util.io.FileUtil;
 import java.io.File;
 import java.util.List;
+import javax.annotation.Nullable;
 
 /** Verifies project views. */
 public class ProjectViewVerifier {
@@ -48,6 +51,7 @@
 
   /** Verifies the project view. Any errors are output to the context as issues. */
   public static boolean verifyProjectView(
+      @Nullable Project project,
       BlazeContext context,
       WorkspacePathResolver workspacePathResolver,
       ProjectViewSet projectViewSet,
@@ -56,10 +60,14 @@
       return false;
     }
     for (BlazeSyncPlugin syncPlugin : BlazeSyncPlugin.EP_NAME.getExtensions()) {
-      if (!syncPlugin.validateProjectView(context, projectViewSet, workspaceLanguageSettings)) {
+      if (!syncPlugin.validateProjectView(
+          project, context, projectViewSet, workspaceLanguageSettings)) {
         return false;
       }
     }
+    if (!LanguageSupport.validateLanguageSettings(context, workspaceLanguageSettings)) {
+      return false;
+    }
     warnAboutDeprecatedSections(context, projectViewSet);
     if (!verifyIncludedPackagesExistOnDisk(context, workspacePathResolver, projectViewSet)) {
       return false;
diff --git a/base/src/com/google/idea/blaze/base/projectview/parser/ParseContext.java b/base/src/com/google/idea/blaze/base/projectview/parser/ParseContext.java
index a7b626a..6956bbb 100644
--- a/base/src/com/google/idea/blaze/base/projectview/parser/ParseContext.java
+++ b/base/src/com/google/idea/blaze/base/projectview/parser/ParseContext.java
@@ -15,6 +15,7 @@
  */
 package com.google.idea.blaze.base.projectview.parser;
 
+import com.google.common.base.Splitter;
 import com.google.common.collect.Lists;
 import com.google.idea.blaze.base.scope.BlazeContext;
 import com.google.idea.blaze.base.scope.output.IssueOutput;
@@ -22,7 +23,7 @@
 import com.google.idea.blaze.base.ui.BlazeValidationError;
 import java.io.File;
 import java.util.List;
-import org.jetbrains.annotations.Nullable;
+import javax.annotation.Nullable;
 
 /** Context for the project view parser. */
 public class ParseContext {
@@ -55,7 +56,7 @@
     this.context = context;
     this.workspacePathResolver = workspacePathResolver;
     this.file = file;
-    this.lines = Lists.newArrayList(text.split("\n"));
+    this.lines = Lists.newArrayList(Splitter.on('\n').split(text));
     this.currentLine = null;
     this.currentLineIndex = -1;
     consume();
diff --git a/base/src/com/google/idea/blaze/base/projectview/parser/ProjectViewParser.java b/base/src/com/google/idea/blaze/base/projectview/parser/ProjectViewParser.java
index 2f4707f..5349ec2 100644
--- a/base/src/com/google/idea/blaze/base/projectview/parser/ProjectViewParser.java
+++ b/base/src/com/google/idea/blaze/base/projectview/parser/ProjectViewParser.java
@@ -68,6 +68,11 @@
   }
 
   public void parseProjectView(String text) {
+    if (text.isEmpty()) {
+      ProjectView projectView = new ProjectView(ImmutableList.of());
+      projectViewFiles.add(new ProjectViewSet.ProjectViewFile(projectView, null));
+      return;
+    }
     parseProjectView(new ParseContext(context, workspacePathResolver, null, text));
   }
 
diff --git a/base/src/com/google/idea/blaze/base/projectview/section/LabelSectionParser.java b/base/src/com/google/idea/blaze/base/projectview/section/LabelSectionParser.java
index bc1ed4a..9c7aafc 100644
--- a/base/src/com/google/idea/blaze/base/projectview/section/LabelSectionParser.java
+++ b/base/src/com/google/idea/blaze/base/projectview/section/LabelSectionParser.java
@@ -38,7 +38,7 @@
       parseContext.addErrors(errors);
       return null;
     }
-    return new Label(text);
+    return Label.create(text);
   }
 
   @Override
diff --git a/base/src/com/google/idea/blaze/base/projectview/section/ListSection.java b/base/src/com/google/idea/blaze/base/projectview/section/ListSection.java
index 40ec7da..d3d9e06 100644
--- a/base/src/com/google/idea/blaze/base/projectview/section/ListSection.java
+++ b/base/src/com/google/idea/blaze/base/projectview/section/ListSection.java
@@ -19,7 +19,9 @@
 import com.google.common.collect.ImmutableList;
 import com.google.idea.blaze.base.projectview.section.sections.ItemOrTextBlock;
 import com.google.idea.blaze.base.projectview.section.sections.TextBlock;
+import java.util.ArrayList;
 import java.util.Collection;
+import java.util.List;
 import java.util.stream.Collectors;
 import javax.annotation.Nullable;
 
@@ -82,7 +84,7 @@
 
   /** Builder for list sections */
   public static class Builder<T> extends SectionBuilder<T, ListSection<T>> {
-    private final ImmutableList.Builder<ItemOrTextBlock<T>> items = ImmutableList.builder();
+    private final List<ItemOrTextBlock<T>> items = new ArrayList<>();
 
     public Builder(SectionKey<T, ListSection<T>> sectionKey, @Nullable ListSection<T> section) {
       super(sectionKey);
@@ -96,14 +98,26 @@
       return this;
     }
 
+    public final Builder<T> addAll(List<T> items) {
+      for (T item : items) {
+        add(item);
+      }
+      return this;
+    }
+
     public final Builder<T> add(TextBlock textBlock) {
       items.add(new ItemOrTextBlock<T>(textBlock));
       return this;
     }
 
+    public final Builder<T> remove(T item) {
+      items.remove(new ItemOrTextBlock<>(item));
+      return this;
+    }
+
     @Override
     public final ListSection<T> build() {
-      return new ListSection<>(getSectionKey(), items.build());
+      return new ListSection<>(getSectionKey(), ImmutableList.copyOf(items));
     }
   }
 }
diff --git a/base/src/com/google/idea/blaze/base/projectview/section/ProjectViewDefaultValueProvider.java b/base/src/com/google/idea/blaze/base/projectview/section/ProjectViewDefaultValueProvider.java
new file mode 100644
index 0000000..370295f
--- /dev/null
+++ b/base/src/com/google/idea/blaze/base/projectview/section/ProjectViewDefaultValueProvider.java
@@ -0,0 +1,32 @@
+/*
+ * Copyright 2017 The Bazel Authors. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.idea.blaze.base.projectview.section;
+
+import com.google.idea.blaze.base.projectview.ProjectView;
+import com.google.idea.blaze.base.projectview.ProjectViewSet;
+import com.google.idea.blaze.base.settings.Blaze.BuildSystem;
+import com.intellij.openapi.extensions.ExtensionPointName;
+
+/** Allows the adding default values to sections. Used during the wizard. */
+public interface ProjectViewDefaultValueProvider {
+  ExtensionPointName<ProjectViewDefaultValueProvider> EP_NAME =
+      ExtensionPointName.create("com.google.idea.blaze.ProjectViewDefaultValueProvider");
+
+  ProjectView addProjectViewDefaultValue(
+      BuildSystem buildSystem, ProjectViewSet projectViewSet, ProjectView topLevelProjectView);
+
+  SectionKey<?, ?> getSectionKey();
+}
diff --git a/base/src/com/google/idea/blaze/base/projectview/section/SectionParser.java b/base/src/com/google/idea/blaze/base/projectview/section/SectionParser.java
index 1e0f043..2c76bb6 100644
--- a/base/src/com/google/idea/blaze/base/projectview/section/SectionParser.java
+++ b/base/src/com/google/idea/blaze/base/projectview/section/SectionParser.java
@@ -15,7 +15,6 @@
  */
 package com.google.idea.blaze.base.projectview.section;
 
-import com.google.idea.blaze.base.projectview.ProjectView;
 import com.google.idea.blaze.base.projectview.parser.ParseContext;
 import com.google.idea.blaze.base.projectview.parser.ProjectViewParser;
 import javax.annotation.Nullable;
@@ -52,9 +51,10 @@
     return null;
   }
 
-  /** Allows the section to add a default value. Used during the wizard. */
-  public ProjectView addProjectViewDefaultValue(ProjectView projectView) {
-    return projectView;
+  /** A brief description of this section, used for in-IDE documentation. */
+  @Nullable
+  public String quickDocs() {
+    return null;
   }
 
   /** The type of item(s) in this section. */
diff --git a/base/src/com/google/idea/blaze/base/projectview/section/sections/AdditionalLanguagesSection.java b/base/src/com/google/idea/blaze/base/projectview/section/sections/AdditionalLanguagesSection.java
index d9dc894..9e05e64 100644
--- a/base/src/com/google/idea/blaze/base/projectview/section/sections/AdditionalLanguagesSection.java
+++ b/base/src/com/google/idea/blaze/base/projectview/section/sections/AdditionalLanguagesSection.java
@@ -15,13 +15,21 @@
  */
 package com.google.idea.blaze.base.projectview.section.sections;
 
+import com.google.common.collect.Ordering;
 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.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.ProjectViewDefaultValueProvider;
 import com.google.idea.blaze.base.projectview.section.SectionKey;
 import com.google.idea.blaze.base.projectview.section.SectionParser;
+import com.google.idea.blaze.base.settings.Blaze.BuildSystem;
+import com.google.idea.blaze.base.sync.projectview.LanguageSupport;
+import java.util.Set;
 import javax.annotation.Nullable;
 
 /** Allows users to set the rule classes they want to be imported */
@@ -56,5 +64,45 @@
     public ItemType getItemType() {
       return ItemType.Other;
     }
+
+    @Override
+    public String quickDocs() {
+      return "Additional languages to support in this project.";
+    }
+  }
+
+  static class AdditionalLanguagesDefaultValueProvider implements ProjectViewDefaultValueProvider {
+    @Override
+    public ProjectView addProjectViewDefaultValue(
+        BuildSystem buildSystem, ProjectViewSet projectViewSet, ProjectView topLevelProjectView) {
+      if (!topLevelProjectView.getSectionsOfType(KEY).isEmpty()) {
+        return topLevelProjectView;
+      }
+      Set<LanguageClass> additionalLanguages = availableAdditionalLanguages(projectViewSet);
+      if (additionalLanguages.isEmpty()) {
+        return topLevelProjectView;
+      }
+      ListSection.Builder<LanguageClass> builder = ListSection.builder(KEY);
+      builder.add(TextBlock.of("  # Uncomment any additional languages you want supported"));
+      additionalLanguages
+          .stream()
+          .sorted(Ordering.usingToString())
+          .map(lang -> "  # " + lang.getName())
+          .forEach(string -> builder.add(TextBlock.of(string)));
+      builder.add(TextBlock.newLine());
+      return ProjectView.builder(topLevelProjectView).add(builder).build();
+    }
+
+    @Override
+    public SectionKey<?, ?> getSectionKey() {
+      return KEY;
+    }
+
+    private static Set<LanguageClass> availableAdditionalLanguages(ProjectViewSet projectView) {
+      WorkspaceType workspaceType =
+          projectView.getScalarValue(
+              WorkspaceTypeSection.KEY, LanguageSupport.getDefaultWorkspaceType());
+      return LanguageSupport.availableAdditionalLanguages(workspaceType);
+    }
   }
 }
diff --git a/base/src/com/google/idea/blaze/base/projectview/section/sections/BuildFlagsSection.java b/base/src/com/google/idea/blaze/base/projectview/section/sections/BuildFlagsSection.java
index 588d289..51f0709 100644
--- a/base/src/com/google/idea/blaze/base/projectview/section/sections/BuildFlagsSection.java
+++ b/base/src/com/google/idea/blaze/base/projectview/section/sections/BuildFlagsSection.java
@@ -48,5 +48,10 @@
     public ItemType getItemType() {
       return ItemType.Other;
     }
+
+    @Override
+    public String quickDocs() {
+      return "A set of flags that get passed to all build command invocations as arguments";
+    }
   }
 }
diff --git a/base/src/com/google/idea/blaze/base/projectview/section/sections/DirectorySection.java b/base/src/com/google/idea/blaze/base/projectview/section/sections/DirectorySection.java
index cc1cb23..c470e37 100644
--- a/base/src/com/google/idea/blaze/base/projectview/section/sections/DirectorySection.java
+++ b/base/src/com/google/idea/blaze/base/projectview/section/sections/DirectorySection.java
@@ -18,12 +18,15 @@
 import com.google.common.collect.Lists;
 import com.google.idea.blaze.base.model.primitives.WorkspacePath;
 import com.google.idea.blaze.base.projectview.ProjectView;
+import com.google.idea.blaze.base.projectview.ProjectViewSet;
 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.ProjectViewDefaultValueProvider;
 import com.google.idea.blaze.base.projectview.section.SectionKey;
 import com.google.idea.blaze.base.projectview.section.SectionParser;
+import com.google.idea.blaze.base.settings.Blaze.BuildSystem;
 import com.google.idea.blaze.base.ui.BlazeValidationError;
 import com.intellij.util.PathUtil;
 import java.util.List;
@@ -69,16 +72,32 @@
     }
 
     @Override
-    public ProjectView addProjectViewDefaultValue(ProjectView projectView) {
-      if (!projectView.getSectionsOfType(KEY).isEmpty()) {
-        return projectView;
+    public String quickDocs() {
+      return "A list of project directories that will be added as source.";
+    }
+  }
+
+  static class DirectoriesProjectViewDefaultValueProvider
+      implements ProjectViewDefaultValueProvider {
+    @Override
+    public ProjectView addProjectViewDefaultValue(
+        BuildSystem buildSystem, ProjectViewSet projectViewSet, ProjectView topLevelProjectView) {
+      if (!topLevelProjectView.getSectionsOfType(KEY).isEmpty()) {
+        return topLevelProjectView;
       }
-      return ProjectView.builder(projectView)
-          .add(
-              ListSection.builder(KEY)
-                  .add(TextBlock.of("  # Add the directories you want added as source here"))
-                  .add(TextBlock.newLine()))
-          .build();
+      ListSection.Builder<DirectoryEntry> builder = ListSection.builder(KEY);
+      builder.add(TextBlock.of("  # Add the directories you want added as source here"));
+      if (buildSystem == BuildSystem.Bazel) {
+        builder.add(TextBlock.of("  # By default, we've added your entire workspace ('.')"));
+        builder.add(DirectoryEntry.include(new WorkspacePath(".")));
+      }
+      builder.add(TextBlock.newLine());
+      return ProjectView.builder(topLevelProjectView).add(builder).build();
+    }
+
+    @Override
+    public SectionKey<?, ?> getSectionKey() {
+      return KEY;
     }
   }
 }
diff --git a/base/src/com/google/idea/blaze/base/projectview/section/sections/ImportSection.java b/base/src/com/google/idea/blaze/base/projectview/section/sections/ImportSection.java
index e377b09..d054235 100644
--- a/base/src/com/google/idea/blaze/base/projectview/section/sections/ImportSection.java
+++ b/base/src/com/google/idea/blaze/base/projectview/section/sections/ImportSection.java
@@ -26,7 +26,7 @@
 import com.google.idea.blaze.base.ui.BlazeValidationError;
 import java.io.File;
 import java.util.List;
-import org.jetbrains.annotations.Nullable;
+import javax.annotation.Nullable;
 
 /** "import" section. */
 public class ImportSection {
@@ -70,5 +70,10 @@
     public ItemType getItemType() {
       return ItemType.FileSystemItem;
     }
+
+    @Override
+    public String quickDocs() {
+      return "Imports another project view.";
+    }
   }
 }
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
index 9ffda84..c3a8ef6 100644
--- 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
@@ -59,5 +59,10 @@
     public ItemType getItemType() {
       return ItemType.FileSystemItem;
     }
+
+    @Override
+    public String quickDocs() {
+      return "A list of XML files which will be imported as run configurations during sync.";
+    }
   }
 }
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 4d071a3..eda173f 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,
-          RunConfigurationsSection.PARSER);
+          RunConfigurationsSection.PARSER,
+          ShardBlazeBuildsSection.PARSER);
 
   public static List<SectionParser> getParsers() {
     List<SectionParser> parsers = Lists.newArrayList(PARSERS);
diff --git a/base/src/com/google/idea/blaze/base/projectview/section/sections/ShardBlazeBuildsSection.java b/base/src/com/google/idea/blaze/base/projectview/section/sections/ShardBlazeBuildsSection.java
new file mode 100644
index 0000000..0b75c8b
--- /dev/null
+++ b/base/src/com/google/idea/blaze/base/projectview/section/sections/ShardBlazeBuildsSection.java
@@ -0,0 +1,65 @@
+/*
+ * Copyright 2017 The Bazel Authors. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.idea.blaze.base.projectview.section.sections;
+
+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.ScalarSection;
+import com.google.idea.blaze.base.projectview.section.ScalarSectionParser;
+import com.google.idea.blaze.base.projectview.section.SectionKey;
+import com.google.idea.blaze.base.projectview.section.SectionParser;
+import javax.annotation.Nullable;
+
+/** Set for particularly large projects to enable sharding blaze invocations during blaze sync. */
+public class ShardBlazeBuildsSection {
+  public static final SectionKey<Boolean, ScalarSection<Boolean>> KEY = SectionKey.of("shard_sync");
+  public static final SectionParser PARSER = new ShardBlazeSyncSectionParser();
+
+  private static class ShardBlazeSyncSectionParser extends ScalarSectionParser<Boolean> {
+    ShardBlazeSyncSectionParser() {
+      super(KEY, ':');
+    }
+
+    @Override
+    @Nullable
+    protected Boolean parseItem(ProjectViewParser parser, ParseContext parseContext, String text) {
+      if (text.equals("true")) {
+        return true;
+      }
+      if (text.equals("false")) {
+        return false;
+      }
+      parseContext.addError(
+          "'shard_sync' must be set to 'true' or 'false' (e.g. 'shard_sync: true')");
+      return null;
+    }
+
+    @Override
+    protected void printItem(StringBuilder sb, Boolean item) {
+      sb.append(item);
+    }
+
+    @Override
+    public ItemType getItemType() {
+      return ItemType.Other;
+    }
+
+    @Override
+    public String quickDocs() {
+      return "Allows sharding build invocations when syncing and compiling your project.";
+    }
+  }
+}
diff --git a/base/src/com/google/idea/blaze/base/projectview/section/sections/TargetSection.java b/base/src/com/google/idea/blaze/base/projectview/section/sections/TargetSection.java
index cd69179..01e38c6 100644
--- a/base/src/com/google/idea/blaze/base/projectview/section/sections/TargetSection.java
+++ b/base/src/com/google/idea/blaze/base/projectview/section/sections/TargetSection.java
@@ -17,12 +17,16 @@
 
 import com.google.idea.blaze.base.model.primitives.TargetExpression;
 import com.google.idea.blaze.base.projectview.ProjectView;
+import com.google.idea.blaze.base.projectview.ProjectViewSet;
 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.ProjectViewDefaultValueProvider;
 import com.google.idea.blaze.base.projectview.section.SectionKey;
 import com.google.idea.blaze.base.projectview.section.SectionParser;
+import com.google.idea.blaze.base.settings.Blaze.BuildSystem;
+import javax.annotation.Nullable;
 
 /** "targets" section. */
 public class TargetSection {
@@ -51,20 +55,35 @@
       return ItemType.Label;
     }
 
+    @Nullable
     @Override
-    public ProjectView addProjectViewDefaultValue(ProjectView projectView) {
-      if (!projectView.getSectionsOfType(KEY).isEmpty()) {
-        return projectView;
+    public String quickDocs() {
+      return "A list of build targets that will be included during sync. To resolve source files "
+          + "under a project directory, the source must be reachable from one of your targets.";
+    }
+  }
+
+  static class TargetsProjectViewDefaultValueProvider implements ProjectViewDefaultValueProvider {
+    @Override
+    public ProjectView addProjectViewDefaultValue(
+        BuildSystem buildSystem, ProjectViewSet projectViewSet, ProjectView topLevelProjectView) {
+      if (!topLevelProjectView.getSectionsOfType(KEY).isEmpty()) {
+        return topLevelProjectView;
       }
-      return ProjectView.builder(projectView)
-          .add(
-              ListSection.builder(KEY)
-                  .add(
-                      TextBlock.of(
-                          "  # Add targets that reach the source code "
-                              + "that you want to resolve here"))
-                  .add(TextBlock.newLine()))
-          .build();
+      ListSection.Builder<TargetExpression> builder = ListSection.builder(KEY);
+      builder.add(
+          TextBlock.of("  # Add targets that reach the source code that you want to resolve here"));
+      if (buildSystem == BuildSystem.Bazel) {
+        builder.add(TextBlock.of("  # By default, we've added all targets in your workspace"));
+        builder.add(TargetExpression.fromString("//..."));
+      }
+      builder.add(TextBlock.newLine());
+      return ProjectView.builder(topLevelProjectView).add(builder).build();
+    }
+
+    @Override
+    public SectionKey<?, ?> getSectionKey() {
+      return KEY;
     }
   }
 }
diff --git a/base/src/com/google/idea/blaze/base/projectview/section/sections/TestSourceSection.java b/base/src/com/google/idea/blaze/base/projectview/section/sections/TestSourceSection.java
index 44da7b0..309b765 100644
--- a/base/src/com/google/idea/blaze/base/projectview/section/sections/TestSourceSection.java
+++ b/base/src/com/google/idea/blaze/base/projectview/section/sections/TestSourceSection.java
@@ -24,5 +24,12 @@
 /** Section for configuring test sources. */
 public class TestSourceSection {
   public static final SectionKey<Glob, ListSection<Glob>> KEY = SectionKey.of("test_sources");
-  public static final SectionParser PARSER = new GlobSectionParser(KEY);
+  public static final SectionParser PARSER =
+      new GlobSectionParser(KEY) {
+        @Override
+        public String quickDocs() {
+          return "A list of workspace-relative glob patterns. Determines which sources IntelliJ "
+              + "treats as test sources.";
+        }
+      };
 }
diff --git a/base/src/com/google/idea/blaze/base/projectview/section/sections/TextBlockSection.java b/base/src/com/google/idea/blaze/base/projectview/section/sections/TextBlockSection.java
index b12b781..1631179 100644
--- a/base/src/com/google/idea/blaze/base/projectview/section/sections/TextBlockSection.java
+++ b/base/src/com/google/idea/blaze/base/projectview/section/sections/TextBlockSection.java
@@ -137,5 +137,11 @@
     public ItemType getItemType() {
       return ItemType.Other;
     }
+
+    @Nullable
+    @Override
+    public String quickDocs() {
+      return null;
+    }
   }
 }
diff --git a/base/src/com/google/idea/blaze/base/run/BlazeCommandRunConfiguration.java b/base/src/com/google/idea/blaze/base/run/BlazeCommandRunConfiguration.java
index 9448702..7f59707 100644
--- a/base/src/com/google/idea/blaze/base/run/BlazeCommandRunConfiguration.java
+++ b/base/src/com/google/idea/blaze/base/run/BlazeCommandRunConfiguration.java
@@ -15,13 +15,18 @@
  */
 package com.google.idea.blaze.base.run;
 
+import static java.util.stream.Collectors.toList;
+
 import com.google.common.base.Strings;
-import com.google.common.collect.Lists;
+import com.google.common.collect.ImmutableList;
 import com.google.idea.blaze.base.ideinfo.TargetIdeInfo;
 import com.google.idea.blaze.base.model.BlazeProjectData;
 import com.google.idea.blaze.base.model.primitives.Kind;
 import com.google.idea.blaze.base.model.primitives.Label;
 import com.google.idea.blaze.base.model.primitives.TargetExpression;
+import com.google.idea.blaze.base.model.primitives.WorkspaceRoot;
+import com.google.idea.blaze.base.projectview.ProjectViewManager;
+import com.google.idea.blaze.base.projectview.ProjectViewSet;
 import com.google.idea.blaze.base.run.confighandler.BlazeCommandRunConfigurationHandler;
 import com.google.idea.blaze.base.run.confighandler.BlazeCommandRunConfigurationHandlerProvider;
 import com.google.idea.blaze.base.run.confighandler.BlazeCommandRunConfigurationRunner;
@@ -29,7 +34,10 @@
 import com.google.idea.blaze.base.run.state.RunConfigurationStateEditor;
 import com.google.idea.blaze.base.run.targetfinder.TargetFinder;
 import com.google.idea.blaze.base.settings.Blaze;
+import com.google.idea.blaze.base.settings.BlazeImportSettings;
+import com.google.idea.blaze.base.settings.BlazeImportSettingsManager;
 import com.google.idea.blaze.base.sync.data.BlazeProjectDataManager;
+import com.google.idea.blaze.base.sync.projectview.ImportRoots;
 import com.google.idea.blaze.base.ui.UiUtil;
 import com.intellij.execution.ExecutionException;
 import com.intellij.execution.Executor;
@@ -55,7 +63,6 @@
 import com.intellij.ui.components.JBLabel;
 import com.intellij.util.ui.UIUtil;
 import java.util.Collection;
-import java.util.List;
 import java.util.Set;
 import java.util.stream.Collectors;
 import javax.annotation.Nullable;
@@ -496,17 +503,27 @@
     }
 
     private static Collection<String> getTargets(Project project) {
-      List<String> result = Lists.newArrayList();
       BlazeProjectData projectData =
           BlazeProjectDataManager.getInstance(project).getBlazeProjectData();
-      if (projectData != null) {
-        for (TargetIdeInfo target : projectData.targetMap.targets()) {
-          if (target.isPlainTarget()) {
-            result.add(target.key.label.toString());
-          }
-        }
+      BlazeImportSettings importSettings =
+          BlazeImportSettingsManager.getInstance(project).getImportSettings();
+      ProjectViewSet projectViewSet = ProjectViewManager.getInstance(project).getProjectViewSet();
+      if (projectData == null || importSettings == null || projectViewSet == null) {
+        return ImmutableList.of();
       }
-      return result;
+      ImportRoots importRoots =
+          ImportRoots.builder(
+                  WorkspaceRoot.fromImportSettings(importSettings), importSettings.getBuildSystem())
+              .add(projectViewSet)
+              .build();
+      return projectData
+          .targetMap
+          .targets()
+          .stream()
+          .filter(TargetIdeInfo::isPlainTarget)
+          .filter(target -> importRoots.importAsSource(target.key.label))
+          .map(target -> target.key.label.toString())
+          .collect(toList());
     }
   }
 }
diff --git a/base/src/com/google/idea/blaze/base/run/BlazeRunConfiguration.java b/base/src/com/google/idea/blaze/base/run/BlazeRunConfiguration.java
index 5a4daed..3b98ced 100644
--- a/base/src/com/google/idea/blaze/base/run/BlazeRunConfiguration.java
+++ b/base/src/com/google/idea/blaze/base/run/BlazeRunConfiguration.java
@@ -16,7 +16,7 @@
 package com.google.idea.blaze.base.run;
 
 import com.google.idea.blaze.base.model.primitives.TargetExpression;
-import org.jetbrains.annotations.Nullable;
+import javax.annotation.Nullable;
 
 /** Marker interface for all run configurations */
 public interface BlazeRunConfiguration {
diff --git a/base/src/com/google/idea/blaze/base/run/BlazeRunConfigurationSyncListener.java b/base/src/com/google/idea/blaze/base/run/BlazeRunConfigurationSyncListener.java
index a1e0549..96be88d 100644
--- a/base/src/com/google/idea/blaze/base/run/BlazeRunConfigurationSyncListener.java
+++ b/base/src/com/google/idea/blaze/base/run/BlazeRunConfigurationSyncListener.java
@@ -34,6 +34,7 @@
 import com.intellij.execution.configurations.RunConfiguration;
 import com.intellij.openapi.project.Project;
 import java.io.File;
+import java.util.LinkedHashSet;
 import java.util.List;
 import java.util.Set;
 import java.util.stream.Collectors;
@@ -67,7 +68,7 @@
 
           Set<Label> labelsWithConfigs = labelsWithConfigs(project);
           Set<TargetExpression> targetExpressions =
-              Sets.newHashSet(projectViewSet.listItems(TargetSection.KEY));
+              Sets.newLinkedHashSet(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)) {
@@ -86,7 +87,7 @@
         .listItems(RunConfigurationsSection.KEY)
         .stream()
         .map(pathResolver::resolveToFile)
-        .collect(Collectors.toSet());
+        .collect(Collectors.toCollection(LinkedHashSet::new));
   }
 
   /** Collects a set of all the Blaze labels that have an associated run configuration. */
@@ -119,7 +120,7 @@
       if (configurationFactory.handlesTarget(project, blazeProjectData, label)) {
         final RunnerAndConfigurationSettings settings =
             configurationFactory.createForTarget(project, runManager, label);
-        runManager.addConfiguration(settings, false /* isShared */);
+        runManager.addConfiguration(settings, /* isShared */ false);
         if (runManager.getSelectedConfiguration() == null) {
           // TODO(joshgiles): Better strategy for picking initially selected config.
           runManager.setSelectedConfiguration(settings);
diff --git a/base/src/com/google/idea/blaze/base/run/TargetNameHeuristic.java b/base/src/com/google/idea/blaze/base/run/TargetNameHeuristic.java
index c2cf40a..bf3cd6c 100644
--- a/base/src/com/google/idea/blaze/base/run/TargetNameHeuristic.java
+++ b/base/src/com/google/idea/blaze/base/run/TargetNameHeuristic.java
@@ -17,7 +17,9 @@
 
 import com.google.idea.blaze.base.ideinfo.TargetIdeInfo;
 import com.google.idea.blaze.base.ideinfo.TestIdeInfo.TestSize;
+import com.intellij.openapi.project.Project;
 import com.intellij.openapi.util.io.FileUtil;
+import com.intellij.psi.PsiFile;
 import java.io.File;
 import javax.annotation.Nullable;
 
@@ -25,7 +27,12 @@
 public class TargetNameHeuristic implements TestTargetHeuristic {
 
   @Override
-  public boolean matchesSource(TargetIdeInfo target, File sourceFile, @Nullable TestSize testSize) {
+  public boolean matchesSource(
+      Project project,
+      TargetIdeInfo target,
+      @Nullable PsiFile sourcePsiFile,
+      File sourceFile,
+      @Nullable TestSize testSize) {
     String filePathWithoutExtension = FileUtil.getNameWithoutExtension(sourceFile.getPath());
     String targetName = target.key.label.targetName().toString();
     if (!filePathWithoutExtension.endsWith(targetName)) {
diff --git a/base/src/com/google/idea/blaze/base/run/TestSizeHeuristic.java b/base/src/com/google/idea/blaze/base/run/TestSizeHeuristic.java
index 67f899a..c30a55e 100644
--- a/base/src/com/google/idea/blaze/base/run/TestSizeHeuristic.java
+++ b/base/src/com/google/idea/blaze/base/run/TestSizeHeuristic.java
@@ -18,6 +18,8 @@
 import com.google.idea.blaze.base.ideinfo.TargetIdeInfo;
 import com.google.idea.blaze.base.ideinfo.TestIdeInfo;
 import com.google.idea.blaze.base.ideinfo.TestIdeInfo.TestSize;
+import com.intellij.openapi.project.Project;
+import com.intellij.psi.PsiFile;
 import java.io.File;
 import javax.annotation.Nullable;
 
@@ -25,7 +27,12 @@
 public class TestSizeHeuristic implements TestTargetHeuristic {
 
   @Override
-  public boolean matchesSource(TargetIdeInfo target, File sourceFile, @Nullable TestSize testSize) {
+  public boolean matchesSource(
+      Project project,
+      TargetIdeInfo target,
+      @Nullable PsiFile sourcePsiFile,
+      File sourceFile,
+      @Nullable TestSize testSize) {
     // If testSize == null then prefer small
     // Some test runners will assume no size annotation == small and filter on that, others will not
     TestSize size = testSize != null ? testSize : TestIdeInfo.DEFAULT_NON_ANNOTATED_TEST_SIZE;
diff --git a/base/src/com/google/idea/blaze/base/run/TestTargetHeuristic.java b/base/src/com/google/idea/blaze/base/run/TestTargetHeuristic.java
index 9f31b39..74a46e4 100644
--- a/base/src/com/google/idea/blaze/base/run/TestTargetHeuristic.java
+++ b/base/src/com/google/idea/blaze/base/run/TestTargetHeuristic.java
@@ -16,9 +16,10 @@
 package com.google.idea.blaze.base.run;
 
 import com.google.idea.blaze.base.ideinfo.TargetIdeInfo;
-import com.google.idea.blaze.base.ideinfo.TestIdeInfo;
+import com.google.idea.blaze.base.ideinfo.TestIdeInfo.TestSize;
 import com.google.idea.blaze.base.model.primitives.Label;
 import com.intellij.openapi.extensions.ExtensionPointName;
+import com.intellij.openapi.project.Project;
 import com.intellij.openapi.vfs.VirtualFile;
 import com.intellij.psi.PsiElement;
 import com.intellij.psi.PsiFile;
@@ -38,22 +39,18 @@
     if (element == null) {
       return null;
     }
-    File file = getContainingFile(element);
-    if (file == null) {
-      return null;
-    }
-    Collection<TargetIdeInfo> rules =
-        TestTargetFinder.getInstance(element.getProject()).testTargetsForSourceFile(file);
-    return chooseTestTargetForSourceFile(file, rules, null);
-  }
-
-  static File getContainingFile(PsiElement element) {
     PsiFile psiFile = element.getContainingFile();
     if (psiFile == null) {
       return null;
     }
     VirtualFile vf = psiFile.getVirtualFile();
-    return vf != null ? new File(vf.getPath()) : null;
+    File file = vf != null ? new File(vf.getPath()) : null;
+    if (file == null) {
+      return null;
+    }
+    Collection<TargetIdeInfo> rules =
+        TestTargetFinder.getInstance(element.getProject()).testTargetsForSourceFile(file);
+    return chooseTestTargetForSourceFile(element.getProject(), psiFile, file, rules, null);
   }
 
   /**
@@ -62,13 +59,19 @@
    */
   @Nullable
   static Label chooseTestTargetForSourceFile(
-      File sourceFile, Collection<TargetIdeInfo> targets, @Nullable TestIdeInfo.TestSize testSize) {
+      Project project,
+      @Nullable PsiFile sourcePsiFile,
+      File sourceFile,
+      Collection<TargetIdeInfo> targets,
+      @Nullable TestSize testSize) {
 
     for (TestTargetHeuristic filter : EP_NAME.getExtensions()) {
       TargetIdeInfo match =
           targets
               .stream()
-              .filter(target -> filter.matchesSource(target, sourceFile, testSize))
+              .filter(
+                  target ->
+                      filter.matchesSource(project, target, sourcePsiFile, sourceFile, testSize))
               .findFirst()
               .orElse(null);
 
@@ -81,5 +84,9 @@
 
   /** Returns true if the rule and source file match, according to this heuristic. */
   boolean matchesSource(
-      TargetIdeInfo target, File sourceFile, @Nullable TestIdeInfo.TestSize testSize);
+      Project project,
+      TargetIdeInfo target,
+      @Nullable PsiFile sourcePsiFile,
+      File sourceFile,
+      @Nullable TestSize testSize);
 }
diff --git a/base/src/com/google/idea/blaze/base/run/TestTargetSourcesHeuristic.java b/base/src/com/google/idea/blaze/base/run/TestTargetSourcesHeuristic.java
new file mode 100644
index 0000000..9a9fb9f
--- /dev/null
+++ b/base/src/com/google/idea/blaze/base/run/TestTargetSourcesHeuristic.java
@@ -0,0 +1,55 @@
+/*
+ * Copyright 2017 The Bazel Authors. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.idea.blaze.base.run;
+
+import com.google.idea.blaze.base.ideinfo.ArtifactLocation;
+import com.google.idea.blaze.base.ideinfo.TargetIdeInfo;
+import com.google.idea.blaze.base.ideinfo.TestIdeInfo.TestSize;
+import com.google.idea.blaze.base.model.BlazeProjectData;
+import com.google.idea.blaze.base.sync.data.BlazeProjectDataManager;
+import com.google.idea.blaze.base.sync.workspace.ArtifactLocationDecoder;
+import com.intellij.openapi.project.Project;
+import com.intellij.psi.PsiFile;
+import java.io.File;
+import javax.annotation.Nullable;
+
+/**
+ * Matches source files to test targets, if the source file is present in the test target's 'srcs'
+ * list. Only looks for exact matches.
+ */
+public class TestTargetSourcesHeuristic implements TestTargetHeuristic {
+
+  @Override
+  public boolean matchesSource(
+      Project project,
+      TargetIdeInfo target,
+      @Nullable PsiFile sourcePsiFile,
+      File sourceFile,
+      @Nullable TestSize testSize) {
+    BlazeProjectData projectData =
+        BlazeProjectDataManager.getInstance(project).getBlazeProjectData();
+    if (projectData == null) {
+      return false;
+    }
+    ArtifactLocationDecoder decoder = projectData.artifactLocationDecoder;
+    for (ArtifactLocation src : target.sources) {
+      if (decoder.decode(src).equals(sourceFile)) {
+        return true;
+      }
+    }
+    return false;
+  }
+}
diff --git a/base/src/com/google/idea/blaze/base/run/WithBrowserHyperlinkExecutionException.java b/base/src/com/google/idea/blaze/base/run/WithBrowserHyperlinkExecutionException.java
new file mode 100644
index 0000000..16b9c23
--- /dev/null
+++ b/base/src/com/google/idea/blaze/base/run/WithBrowserHyperlinkExecutionException.java
@@ -0,0 +1,49 @@
+/*
+ * Copyright 2017 The Bazel Authors. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.idea.blaze.base.run;
+
+import com.intellij.execution.ExecutionException;
+import com.intellij.notification.Notification;
+import com.intellij.notification.NotificationListener;
+import com.intellij.ui.BrowserHyperlinkListener;
+import javax.swing.event.HyperlinkEvent;
+import javax.swing.event.HyperlinkListener;
+import org.jetbrains.annotations.NotNull;
+
+/**
+ * An {@link ExecutionException} containing a clickable browser hyperlink. It attempts to navigate
+ * to a URL formed from the hyperlink description verbatim.
+ */
+public class WithBrowserHyperlinkExecutionException extends ExecutionException
+    implements HyperlinkListener, NotificationListener {
+
+  public WithBrowserHyperlinkExecutionException(String string) {
+    super(string);
+  }
+
+  @Override
+  public final void hyperlinkUpdate(HyperlinkEvent e) {
+    if (e.getEventType() == HyperlinkEvent.EventType.ACTIVATED) {
+      BrowserHyperlinkListener.INSTANCE.hyperlinkUpdate(e);
+    }
+  }
+
+  @Override
+  public final void hyperlinkUpdate(
+      @NotNull Notification notification, @NotNull HyperlinkEvent event) {
+    hyperlinkUpdate(event);
+  }
+}
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 da3d641..6d8dcd7 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
@@ -72,7 +72,7 @@
   @Override
   @Nullable
   public String getCommandName() {
-    BlazeCommandName command = state.getCommand();
+    BlazeCommandName command = state.getCommandState().getCommand();
     return command != null ? command.toString() : null;
   }
 
diff --git a/base/src/com/google/idea/blaze/base/run/confighandler/BlazeCommandGenericRunConfigurationRunner.java b/base/src/com/google/idea/blaze/base/run/confighandler/BlazeCommandGenericRunConfigurationRunner.java
index 7c508e2..4605c17 100644
--- a/base/src/com/google/idea/blaze/base/run/confighandler/BlazeCommandGenericRunConfigurationRunner.java
+++ b/base/src/com/google/idea/blaze/base/run/confighandler/BlazeCommandGenericRunConfigurationRunner.java
@@ -38,7 +38,6 @@
 import com.google.idea.blaze.base.settings.Blaze;
 import com.google.idea.blaze.base.settings.BlazeImportSettings;
 import com.google.idea.blaze.base.settings.BlazeImportSettingsManager;
-import com.google.idea.common.experiments.BoolExperiment;
 import com.intellij.execution.DefaultExecutionResult;
 import com.intellij.execution.ExecutionException;
 import com.intellij.execution.ExecutionResult;
@@ -65,9 +64,6 @@
 public final class BlazeCommandGenericRunConfigurationRunner
     implements BlazeCommandRunConfigurationRunner {
 
-  private static final BoolExperiment smRunnerUiEnabled =
-      new BoolExperiment("use.smrunner.ui.general", true);
-
   @Override
   public RunProfileState getRunProfileState(Executor executor, ExecutionEnvironment environment) {
     return new BlazeCommandRunProfileState(environment, ImmutableList.of());
@@ -152,9 +148,7 @@
           new ScopedBlazeProcessHandler.ScopedProcessHandlerDelegate() {
             @Override
             public void onBlazeContextStart(BlazeContext context) {
-              context
-                  .push(new IssuesScope(project))
-                  .push(new IdeaLogScope());
+              context.push(new IssuesScope(project)).push(new IdeaLogScope());
             }
 
             @Override
@@ -171,29 +165,28 @@
       ProjectViewSet projectViewSet = ProjectViewManager.getInstance(project).getProjectViewSet();
       assert projectViewSet != null;
 
+      String binaryPath =
+          handlerState.getBlazeBinaryState().getBlazeBinary() != null
+              ? handlerState.getBlazeBinaryState().getBlazeBinary()
+              : Blaze.getBuildSystemProvider(project).getBinaryPath();
+
       BlazeCommand.Builder command =
-          BlazeCommand.builder(Blaze.getBuildSystem(project), handlerState.getCommand())
-              .setBlazeBinary(handlerState.getBlazeBinary())
+          BlazeCommand.builder(binaryPath, handlerState.getCommandState().getCommand())
               .addTargets(configuration.getTarget())
               .addBlazeFlags(BlazeFlags.buildFlags(project, projectViewSet))
               .addBlazeFlags(testHandlerFlags)
-              .addBlazeFlags(handlerState.getBlazeFlags())
-              .addExeFlags(handlerState.getExeFlags());
+              .addBlazeFlags(handlerState.getBlazeFlagsState().getExpandedFlags())
+              .addExeFlags(handlerState.getExeFlagsState().getExpandedFlags());
 
-      boolean runDistributed = handlerState.getRunOnDistributedExecutor();
       command.addBlazeFlags(
           DistributedExecutorSupport.getBlazeFlags(
-              project, handlerState.getRunOnDistributedExecutor()));
-      if (!runDistributed) {
-        command.addBlazeFlags(BlazeFlags.TEST_OUTPUT_STREAMED);
-      }
+              project, handlerState.getRunOnDistributedExecutorState().runOnDistributedExecutor));
       return command.build();
     }
 
     private boolean canUseTestUi() {
-      return smRunnerUiEnabled.getValue()
-          && BlazeCommandName.TEST.equals(handlerState.getCommand())
-          && !handlerState.getRunOnDistributedExecutor();
+      return BlazeCommandName.TEST.equals(handlerState.getCommandState().getCommand())
+          && !handlerState.getRunOnDistributedExecutorState().runOnDistributedExecutor;
     }
   }
 }
diff --git a/base/src/com/google/idea/blaze/base/run/filter/BlazeTargetFilter.java b/base/src/com/google/idea/blaze/base/run/filter/BlazeTargetFilter.java
index 1cf0e0c..2e4f9ea 100644
--- a/base/src/com/google/idea/blaze/base/run/filter/BlazeTargetFilter.java
+++ b/base/src/com/google/idea/blaze/base/run/filter/BlazeTargetFilter.java
@@ -15,6 +15,7 @@
  */
 package com.google.idea.blaze.base.run.filter;
 
+import com.google.common.annotations.VisibleForTesting;
 import com.google.idea.blaze.base.lang.buildfile.references.BuildReferenceManager;
 import com.google.idea.blaze.base.lang.buildfile.references.LabelUtils;
 import com.google.idea.blaze.base.model.primitives.Label;
@@ -23,14 +24,24 @@
 import com.intellij.openapi.project.Project;
 import com.intellij.psi.NavigatablePsiElement;
 import com.intellij.psi.PsiElement;
+import java.util.ArrayList;
+import java.util.List;
 import java.util.regex.Matcher;
 import java.util.regex.Pattern;
-import org.jetbrains.annotations.Nullable;
+import javax.annotation.Nullable;
 
 /** Parse blaze targets in streamed output. */
 public class BlazeTargetFilter implements Filter {
 
-  private static final Pattern TARGET_PATTERN = Pattern.compile("//([^\\s:]*):(\\S*)");
+  // See Bazel's LabelValidator class. Whitespace character intentionally not included here.
+  private static final String PACKAGE_NAME_CHARS = "a-zA-Z0-9/\\-\\._$()";
+  private static final String TARGET_CHARS = "a-zA-Z0-9+,=~#()$_@\\-";
+
+  private static final String TARGET_REGEX =
+      String.format(
+          "(@[%s]*)?//[%s]*(:[%s]*)?", PACKAGE_NAME_CHARS, PACKAGE_NAME_CHARS, TARGET_CHARS);
+
+  @VisibleForTesting static final Pattern TARGET_PATTERN = Pattern.compile(TARGET_REGEX);
 
   private final Project project;
 
@@ -42,20 +53,21 @@
   @Override
   public Result applyFilter(String line, int entireLength) {
     Matcher matcher = TARGET_PATTERN.matcher(line);
-    if (!matcher.find()) {
-      return null;
+    List<ResultItem> results = new ArrayList<>();
+    while (matcher.find()) {
+      String labelString = matcher.group();
+      Label label = LabelUtils.createLabelFromString(null, labelString);
+      if (label == null) {
+        continue;
+      }
+      PsiElement psi = BuildReferenceManager.getInstance(project).resolveLabel(label);
+      if (!(psi instanceof NavigatablePsiElement)) {
+        continue;
+      }
+      HyperlinkInfo link = project -> ((NavigatablePsiElement) psi).navigate(true);
+      int offset = entireLength - line.length();
+      results.add(new ResultItem(matcher.start() + offset, matcher.end() + offset, link));
     }
-    String labelString = matcher.group();
-    Label label = LabelUtils.createLabelFromString(null, labelString);
-    if (label == null) {
-      return null;
-    }
-    PsiElement psi = BuildReferenceManager.getInstance(project).resolveLabel(label);
-    if (!(psi instanceof NavigatablePsiElement)) {
-      return null;
-    }
-    HyperlinkInfo link = project -> ((NavigatablePsiElement) psi).navigate(true);
-    int offset = entireLength - line.length();
-    return new Result(matcher.start() + offset, matcher.end() + offset, link);
+    return results.isEmpty() ? null : new Result(results);
   }
 }
diff --git a/base/src/com/google/idea/blaze/base/run/filter/StandardFileResolver.java b/base/src/com/google/idea/blaze/base/run/filter/StandardFileResolver.java
index 15a3d09..652c2a5 100644
--- a/base/src/com/google/idea/blaze/base/run/filter/StandardFileResolver.java
+++ b/base/src/com/google/idea/blaze/base/run/filter/StandardFileResolver.java
@@ -21,6 +21,7 @@
 import com.intellij.openapi.project.Project;
 import com.intellij.openapi.vfs.VirtualFile;
 import java.io.File;
+import java.io.IOException;
 import javax.annotation.Nullable;
 
 /** Parses absolute and workspace-relative paths. */
@@ -31,7 +32,9 @@
   public VirtualFile resolveToFile(Project project, String fileString) {
     File file = new File(fileString);
     if (file.isAbsolute()) {
-      return VirtualFileSystemProvider.getInstance().getSystem().findFileByPath(file.getPath());
+      return VirtualFileSystemProvider.getInstance()
+          .getSystem()
+          .findFileByPath(getCanonicalPathSafe(file));
     }
     BlazeProjectData projectData =
         BlazeProjectDataManager.getInstance(project).getBlazeProjectData();
@@ -41,4 +44,16 @@
     file = projectData.workspacePathResolver.resolveToFile(fileString);
     return VirtualFileSystemProvider.getInstance().getSystem().findFileByPath(file.getPath());
   }
+
+  /**
+   * Swallows {@link IOException}s, falling back to returning the absolute, possibly non-canonical
+   * path.
+   */
+  private static String getCanonicalPathSafe(File file) {
+    try {
+      return file.getCanonicalPath();
+    } catch (IOException e) {
+      return file.getAbsolutePath();
+    }
+  }
 }
diff --git a/base/src/com/google/idea/blaze/base/run/producers/AllInPackageBlazeConfigurationProducer.java b/base/src/com/google/idea/blaze/base/run/producers/AllInPackageBlazeConfigurationProducer.java
index 154a9be..d344018 100644
--- a/base/src/com/google/idea/blaze/base/run/producers/AllInPackageBlazeConfigurationProducer.java
+++ b/base/src/com/google/idea/blaze/base/run/producers/AllInPackageBlazeConfigurationProducer.java
@@ -16,6 +16,7 @@
 package com.google.idea.blaze.base.run.producers;
 
 import com.google.idea.blaze.base.command.BlazeCommandName;
+import com.google.idea.blaze.base.lang.buildfile.search.BlazePackage;
 import com.google.idea.blaze.base.model.primitives.TargetExpression;
 import com.google.idea.blaze.base.model.primitives.WorkspacePath;
 import com.google.idea.blaze.base.model.primitives.WorkspaceRoot;
@@ -61,7 +62,7 @@
     if (handlerState == null) {
       return false;
     }
-    handlerState.setCommand(BlazeCommandName.TEST);
+    handlerState.getCommandState().setCommand(BlazeCommandName.TEST);
     configuration.setGeneratedName();
     return true;
   }
@@ -84,7 +85,7 @@
     if (handlerState == null) {
       return false;
     }
-    return Objects.equals(handlerState.getCommand(), BlazeCommandName.TEST)
+    return Objects.equals(handlerState.getCommandState().getCommand(), BlazeCommandName.TEST)
         && Objects.equals(
             configuration.getTarget(), TargetExpression.allFromPackageRecursive(packagePath));
   }
@@ -93,13 +94,11 @@
   private static PsiDirectory getTestDirectory(ConfigurationContext context) {
     WorkspaceRoot root = WorkspaceRoot.fromProject(context.getModule().getProject());
     PsiElement location = context.getPsiLocation();
-    if (location instanceof PsiDirectory) {
-      PsiDirectory dir = (PsiDirectory) location;
-      if (isInWorkspace(root, dir)) {
-        return dir;
-      }
+    if (!(location instanceof PsiDirectory)) {
+      return null;
     }
-    return null;
+    PsiDirectory dir = (PsiDirectory) location;
+    return isInWorkspace(root, dir) && BlazePackage.hasBlazePackageChild(dir) ? dir : null;
   }
 
   @Nullable
diff --git a/base/src/com/google/idea/blaze/base/run/producers/BlazeBuildFileRunConfigurationProducer.java b/base/src/com/google/idea/blaze/base/run/producers/BlazeBuildFileRunConfigurationProducer.java
index 60faff7..9a838ed 100644
--- a/base/src/com/google/idea/blaze/base/run/producers/BlazeBuildFileRunConfigurationProducer.java
+++ b/base/src/com/google/idea/blaze/base/run/producers/BlazeBuildFileRunConfigurationProducer.java
@@ -195,8 +195,9 @@
         configuration.getHandlerStateIfType(BlazeCommandRunConfigurationCommonState.class);
     if (handlerState != null) {
       // TODO move the old test rule functionality to a BlazeRunConfigurationFactory
-      handlerState.setCommand(
-          Kind.isTestRule(target.ruleType) ? BlazeCommandName.TEST : BlazeCommandName.BUILD);
+      BlazeCommandName command =
+          Kind.isTestRule(target.ruleType) ? BlazeCommandName.TEST : BlazeCommandName.BUILD;
+      handlerState.getCommandState().setCommand(command);
     }
     configuration.setGeneratedName();
   }
diff --git a/base/src/com/google/idea/blaze/base/run/producers/BlazeFilterExistingRunConfigurationProducer.java b/base/src/com/google/idea/blaze/base/run/producers/BlazeFilterExistingRunConfigurationProducer.java
index feae47b..92089e4 100644
--- a/base/src/com/google/idea/blaze/base/run/producers/BlazeFilterExistingRunConfigurationProducer.java
+++ b/base/src/com/google/idea/blaze/base/run/producers/BlazeFilterExistingRunConfigurationProducer.java
@@ -59,14 +59,20 @@
     }
     BlazeCommandRunConfigurationCommonState handlerState =
         configuration.getHandlerStateIfType(BlazeCommandRunConfigurationCommonState.class);
-    if (handlerState == null || !BlazeCommandName.TEST.equals(handlerState.getCommand())) {
+    if (handlerState == null
+        || !BlazeCommandName.TEST.equals(handlerState.getCommandState().getCommand())) {
       return false;
     }
     // replace old test filter flag if present
-    List<String> flags = new ArrayList<>(handlerState.getBlazeFlags());
+    List<String> flags = new ArrayList<>(handlerState.getBlazeFlagsState().getRawFlags());
     flags.removeIf((flag) -> flag.startsWith(BlazeFlags.TEST_FILTER));
     flags.add(testFilter);
-    handlerState.setBlazeFlags(flags);
+
+    if (SmRunnerUtils.countSelectedTestCases(context) == 1
+        && !flags.contains(BlazeFlags.DISABLE_TEST_SHARDING)) {
+      flags.add(BlazeFlags.DISABLE_TEST_SHARDING);
+    }
+    handlerState.getBlazeFlagsState().setRawFlags(flags);
     configuration.setName(configuration.getName() + " (filtered)");
     configuration.setNameChangedByUser(true);
     return true;
@@ -83,7 +89,7 @@
         configuration.getHandlerStateIfType(BlazeCommandRunConfigurationCommonState.class);
 
     return handlerState != null
-        && Objects.equals(handlerState.getCommand(), BlazeCommandName.TEST)
+        && Objects.equals(handlerState.getCommandState().getCommand(), BlazeCommandName.TEST)
         && Objects.equals(testFilter, handlerState.getTestFilterFlag());
   }
 
diff --git a/base/src/com/google/idea/blaze/base/run/smrunner/BlazeCompositeTestEventsHandler.java b/base/src/com/google/idea/blaze/base/run/smrunner/BlazeCompositeTestEventsHandler.java
index 5d86bdf..21bf76a 100644
--- a/base/src/com/google/idea/blaze/base/run/smrunner/BlazeCompositeTestEventsHandler.java
+++ b/base/src/com/google/idea/blaze/base/run/smrunner/BlazeCompositeTestEventsHandler.java
@@ -20,6 +20,7 @@
 import com.google.common.collect.ImmutableSet;
 import com.google.common.collect.Maps;
 import com.google.idea.blaze.base.model.primitives.Kind;
+import com.google.idea.blaze.base.run.smrunner.BlazeXmlSchema.TestSuite;
 import com.intellij.execution.Location;
 import com.intellij.execution.testframework.actions.AbstractRerunFailedTestsAction;
 import com.intellij.execution.testframework.sm.runner.SMTestLocator;
@@ -95,6 +96,12 @@
     return null;
   }
 
+  @Override
+  public boolean ignoreSuite(@Nullable Kind kind, TestSuite suite) {
+    BlazeTestEventsHandler handler = kind != null ? getHandlers().get(kind) : null;
+    return handler != null ? handler.ignoreSuite(kind, suite) : super.ignoreSuite(kind, suite);
+  }
+
   /** Converts the testsuite name in the blaze test XML to a user-friendly format */
   @Override
   public String suiteDisplayName(@Nullable Kind kind, String rawName) {
diff --git a/base/src/com/google/idea/blaze/base/run/smrunner/BlazeRerunFailedTestsAction.java b/base/src/com/google/idea/blaze/base/run/smrunner/BlazeRerunFailedTestsAction.java
index 2522bab..578bb9b 100644
--- a/base/src/com/google/idea/blaze/base/run/smrunner/BlazeRerunFailedTestsAction.java
+++ b/base/src/com/google/idea/blaze/base/run/smrunner/BlazeRerunFailedTestsAction.java
@@ -80,13 +80,15 @@
         throws ExecutionException {
       BlazeCommandRunConfigurationCommonState handlerState =
           configuration.getHandlerStateIfType(BlazeCommandRunConfigurationCommonState.class);
-      if (handlerState == null || !BlazeCommandName.TEST.equals(handlerState.getCommand())) {
+      if (handlerState == null
+          || !BlazeCommandName.TEST.equals(handlerState.getCommandState().getCommand())) {
         return null;
       }
       Project project = getProject();
       List<Location<?>> locations =
           getFailedTests(project)
               .stream()
+              .filter(AbstractTestProxy::isLeaf)
               .map((test) -> toLocation(project, test))
               .filter(Objects::nonNull)
               .collect(Collectors.toList());
@@ -94,8 +96,9 @@
       if (testFilter == null) {
         return null;
       }
-      List<String> blazeFlags = setTestFilter(handlerState.getBlazeFlags(), testFilter);
-      handlerState.setBlazeFlags(blazeFlags);
+      List<String> blazeFlags =
+          setTestFilter(handlerState.getBlazeFlagsState().getRawFlags(), testFilter);
+      handlerState.getBlazeFlagsState().setRawFlags(blazeFlags);
       return configuration.getState(executor, environment);
     }
 
diff --git a/base/src/com/google/idea/blaze/base/run/smrunner/BlazeTestEventsHandler.java b/base/src/com/google/idea/blaze/base/run/smrunner/BlazeTestEventsHandler.java
index 2f3286f..3ecdeea 100644
--- a/base/src/com/google/idea/blaze/base/run/smrunner/BlazeTestEventsHandler.java
+++ b/base/src/com/google/idea/blaze/base/run/smrunner/BlazeTestEventsHandler.java
@@ -44,18 +44,17 @@
 
   /**
    * Blaze/Bazel flags required for test UI.<br>
-   * Forces local test execution, without sharding.
+   * Forces local test execution, without retries.
    */
   public static ImmutableList<String> getBlazeFlags(Project project) {
     ImmutableList.Builder<String> flags =
-        ImmutableList.<String>builder()
-            .add(
-                "--test_sharding_strategy=disabled",
-                "--runs_per_test=1",
-                "--flaky_test_attempts=1");
+        ImmutableList.<String>builder().add("--runs_per_test=1", "--flaky_test_attempts=1");
     if (Blaze.getBuildSystem(project) == BuildSystem.Blaze) {
       flags.add("--test_strategy=local");
     }
+    if (Blaze.getBuildSystem(project) == BuildSystem.Bazel) {
+      flags.add("--test_sharding_strategy=disabled");
+    }
     return flags.build();
   }
 
@@ -117,16 +116,16 @@
 
   public String testLocationUrl(
       @Nullable Kind kind, String parentSuite, String name, @Nullable String className) {
-    String base = SmRunnerUtils.GENERIC_TEST_PROTOCOL + URLUtil.SCHEME_SEPARATOR + name;
+    String base = SmRunnerUtils.GENERIC_TEST_PROTOCOL + URLUtil.SCHEME_SEPARATOR;
     if (Strings.isNullOrEmpty(className)) {
-      return base;
+      return base + name;
     }
-    return base + SmRunnerUtils.TEST_NAME_PARTS_SPLITTER + className;
+    return base + className + SmRunnerUtils.TEST_NAME_PARTS_SPLITTER + name;
   }
 
   /** Whether to skip logging a {@link TestSuite}. */
-  public boolean ignoreSuite(TestSuite suite) {
+  public boolean ignoreSuite(@Nullable Kind kind, TestSuite suite) {
     // by default only include innermost 'testsuite' elements
-    return suite.testSuites.isEmpty();
+    return !suite.testSuites.isEmpty();
   }
 }
diff --git a/base/src/com/google/idea/blaze/base/run/smrunner/BlazeXmlSchema.java b/base/src/com/google/idea/blaze/base/run/smrunner/BlazeXmlSchema.java
index 4126bc8..f1f053a 100644
--- a/base/src/com/google/idea/blaze/base/run/smrunner/BlazeXmlSchema.java
+++ b/base/src/com/google/idea/blaze/base/run/smrunner/BlazeXmlSchema.java
@@ -18,6 +18,7 @@
 import com.google.common.collect.Lists;
 import java.io.InputStream;
 import java.util.List;
+import java.util.Objects;
 import javax.xml.bind.JAXBContext;
 import javax.xml.bind.JAXBException;
 import javax.xml.bind.annotation.XmlAttribute;
@@ -95,6 +96,40 @@
 
     @XmlElement(name = "testcase")
     List<TestCase> testCases = Lists.newArrayList();
+
+    /** Used to merge test suites from a single target, split across multiple shards */
+    private void addSuite(TestSuite suite) {
+      for (TestSuite existing : testSuites) {
+        if (Objects.equals(existing.name, suite.name)) {
+          existing.mergeWithSuite(suite);
+          return;
+        }
+      }
+      testSuites.add(suite);
+    }
+
+    private void mergeWithSuite(TestSuite suite) {
+      for (TestSuite child : suite.testSuites) {
+        addSuite(child);
+      }
+      testDecorators.addAll(suite.testDecorators);
+      testCases.addAll(suite.testCases);
+      tests += suite.tests;
+      failures += suite.failures;
+      errors += suite.errors;
+      skipped += suite.skipped;
+      disabled += suite.disabled;
+      time += suite.time;
+    }
+  }
+
+  /** Used to merge test suites from a single target, split across multiple shards */
+  static TestSuite mergeSuites(List<TestSuite> suites) {
+    TestSuite outer = new TestSuite();
+    for (TestSuite suite : suites) {
+      outer.addSuite(suite);
+    }
+    return outer;
   }
 
   static class TestCase {
diff --git a/base/src/com/google/idea/blaze/base/run/smrunner/BlazeXmlToTestEventsConverter.java b/base/src/com/google/idea/blaze/base/run/smrunner/BlazeXmlToTestEventsConverter.java
index 548557a..0cdee3b 100644
--- a/base/src/com/google/idea/blaze/base/run/smrunner/BlazeXmlToTestEventsConverter.java
+++ b/base/src/com/google/idea/blaze/base/run/smrunner/BlazeXmlToTestEventsConverter.java
@@ -22,8 +22,8 @@
 import com.google.idea.blaze.base.run.smrunner.BlazeXmlSchema.TestCase;
 import com.google.idea.blaze.base.run.smrunner.BlazeXmlSchema.TestSuite;
 import com.google.idea.blaze.base.run.targetfinder.TargetFinder;
-import com.google.idea.blaze.base.run.testlogs.BlazeTestXmlFinderStrategy;
-import com.google.idea.blaze.base.run.testlogs.CompletedTestTarget;
+import com.google.idea.blaze.base.run.testlogs.BlazeTestResultFinderStrategy;
+import com.google.idea.blaze.base.run.testlogs.BlazeTestResults;
 import com.google.idea.sdkcompat.smrunner.SmRunnerCompatUtils;
 import com.intellij.execution.process.ProcessOutputTypes;
 import com.intellij.execution.testframework.TestConsoleProperties;
@@ -37,9 +37,13 @@
 import com.intellij.execution.testframework.sm.runner.events.TestSuiteStartedEvent;
 import com.intellij.openapi.project.Project;
 import com.intellij.openapi.util.Key;
+import java.io.File;
 import java.io.FileInputStream;
 import java.io.InputStream;
 import java.text.ParseException;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
 import javax.annotation.Nullable;
 import jetbrains.buildServer.messages.serviceMessages.ServiceMessageVisitor;
 import jetbrains.buildServer.messages.serviceMessages.TestSuiteStarted;
@@ -87,14 +91,46 @@
     onStartTesting();
     getProcessor().onTestsReporterAttached();
 
-    for (CompletedTestTarget testTarget : BlazeTestXmlFinderStrategy.locateTestXmlFiles(project)) {
-      try (InputStream input = new FileInputStream(testTarget.testResultXml)) {
-        parseXmlInput(getProcessor(), getKind(project, testTarget.label), input);
+    BlazeTestResults testResults = BlazeTestResultFinderStrategy.locateTestResults(project);
+    for (Label target : testResults.failedTargets) {
+      reportFailedTarget(target);
+    }
+    for (Label label : testResults.testXmlFiles.keySet()) {
+      processTestSuites(label, testResults.testXmlFiles.get(label));
+    }
+  }
+
+  private void reportFailedTarget(Label label) {
+    GeneralTestEventsProcessor processor = getProcessor();
+    TestSuiteStarted suiteStarted = new TestSuiteStarted(label.toString());
+    processor.onSuiteStarted(new TestSuiteStartedEvent(suiteStarted, null));
+    String targetName = label.targetName().toString();
+    processor.onTestStarted(new TestStartedEvent(targetName, null));
+    processor.onTestFailure(
+        SmRunnerCompatUtils.getTestFailedEvent(
+            targetName, "Target failed to build. See console output for details", null, 0));
+    processor.onTestFinished(new TestFinishedEvent(targetName, 0L));
+    processor.onSuiteFinished(new TestSuiteFinishedEvent(label.toString()));
+  }
+
+  /** Process all test XML files from a single test target. */
+  private void processTestSuites(Label label, Collection<File> files) {
+    Kind kind = getKind(project, label);
+    List<TestSuite> targetSuites = new ArrayList<>();
+    for (File file : files) {
+      try (InputStream input = new FileInputStream(file)) {
+        targetSuites.add(BlazeXmlSchema.parse(input));
       } catch (Exception e) {
         // ignore parsing errors -- most common cause is user cancellation, which we can't easily
         // recognize.
       }
     }
+    if (targetSuites.isEmpty()) {
+      return;
+    }
+    TestSuite suite =
+        targetSuites.size() == 1 ? targetSuites.get(0) : BlazeXmlSchema.mergeSuites(targetSuites);
+    processTestSuite(getProcessor(), kind, suite);
   }
 
   @Nullable
@@ -103,19 +139,13 @@
     return target != null ? target.kind : null;
   }
 
-  private void parseXmlInput(
-      GeneralTestEventsProcessor processor, @Nullable Kind kind, InputStream input) {
-    TestSuite testResult = BlazeXmlSchema.parse(input);
-    processTestSuite(processor, kind, testResult);
-  }
-
   private void processTestSuite(
       GeneralTestEventsProcessor processor, @Nullable Kind kind, TestSuite suite) {
     if (!hasRunChild(suite)) {
       return;
     }
     // only include the innermost 'testsuite' element
-    boolean logSuite = !eventsHandler.ignoreSuite(suite);
+    boolean logSuite = !eventsHandler.ignoreSuite(kind, suite);
     if (suite.name != null && logSuite) {
       TestSuiteStarted suiteStarted =
           new TestSuiteStarted(eventsHandler.suiteDisplayName(kind, suite.name));
diff --git a/base/src/com/google/idea/blaze/base/run/smrunner/SmRunnerUtils.java b/base/src/com/google/idea/blaze/base/run/smrunner/SmRunnerUtils.java
index b9e80ba..54f6595 100644
--- a/base/src/com/google/idea/blaze/base/run/smrunner/SmRunnerUtils.java
+++ b/base/src/com/google/idea/blaze/base/run/smrunner/SmRunnerUtils.java
@@ -33,8 +33,10 @@
 import com.intellij.openapi.util.Disposer;
 import com.intellij.psi.search.GlobalSearchScope;
 import java.util.Arrays;
+import java.util.HashSet;
 import java.util.List;
 import java.util.Objects;
+import java.util.Set;
 import java.util.stream.Collectors;
 import javax.annotation.Nullable;
 import javax.swing.tree.TreePath;
@@ -88,6 +90,26 @@
   }
 
   public static List<Location<?>> getSelectedSmRunnerTreeElements(ConfigurationContext context) {
+    Project project = context.getProject();
+    List<SMTestProxy> tests = getSelectedTestProxies(context);
+    return tests
+        .stream()
+        .map(test -> (Location<?>) test.getLocation(project, GlobalSearchScope.allScope(project)))
+        .filter(Objects::nonNull)
+        .collect(Collectors.toList());
+  }
+
+  /** Counts all selected test cases, and their children, recursively */
+  public static int countSelectedTestCases(ConfigurationContext context) {
+    List<SMTestProxy> tests = getSelectedTestProxies(context);
+    Set<SMTestProxy> allTests = new HashSet<>(tests);
+    for (SMTestProxy test : tests) {
+      allTests.addAll(test.collectChildren());
+    }
+    return allTests.size();
+  }
+
+  private static List<SMTestProxy> getSelectedTestProxies(ConfigurationContext context) {
     SMTRunnerTestTreeView treeView =
         SMTRunnerTestTreeView.SM_TEST_RUNNER_VIEW.getData(context.getDataContext());
     if (treeView == null) {
@@ -98,18 +120,16 @@
       return ImmutableList.of();
     }
     return Arrays.stream(paths)
-        .map((path) -> toLocation(context.getProject(), treeView, path))
+        .map((path) -> toTestProxy(treeView, path))
         .filter(Objects::nonNull)
         .collect(Collectors.toList());
   }
 
   @Nullable
-  private static Location<?> toLocation(
-      Project project, SMTRunnerTestTreeView treeView, TreePath path) {
+  private static SMTestProxy toTestProxy(SMTRunnerTestTreeView treeView, TreePath path) {
     if (treeView.isPathSelected(path.getParentPath())) {
       return null;
     }
-    SMTestProxy test = treeView.getSelectedTest(path);
-    return test != null ? test.getLocation(project, GlobalSearchScope.allScope(project)) : null;
+    return treeView.getSelectedTest(path);
   }
 }
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 ef2ec8e..082f4be 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
@@ -23,7 +23,6 @@
 import com.intellij.execution.configurations.RuntimeConfigurationException;
 import com.intellij.openapi.project.Project;
 import java.io.File;
-import java.util.List;
 import javax.annotation.Nullable;
 
 /**
@@ -49,46 +48,32 @@
     addStates(command, blazeFlags, exeFlags, blazeBinary, runOnDistributedExecutor);
   }
 
-  @Nullable
-  public BlazeCommandName getCommand() {
-    return command.getCommand();
-  }
-
   /** @return The list of blaze flags that the user specified manually. */
-  public List<String> getBlazeFlags() {
-    return blazeFlags.getFlags();
+  public RunConfigurationFlagsState getBlazeFlagsState() {
+    return blazeFlags;
   }
 
   /** @return The list of executable flags the user specified manually. */
-  public List<String> getExeFlags() {
-    return exeFlags.getFlags();
+  public RunConfigurationFlagsState getExeFlagsState() {
+    return exeFlags;
   }
 
-  @Nullable
-  public String getBlazeBinary() {
-    return blazeBinary.getBlazeBinary();
+  public BlazeBinaryState getBlazeBinaryState() {
+    return blazeBinary;
   }
 
-  public void setCommand(@Nullable BlazeCommandName command) {
-    this.command.setCommand(command);
+  public BlazeCommandState getCommandState() {
+    return command;
   }
 
-  public void setBlazeFlags(List<String> flags) {
-    this.blazeFlags.setFlags(flags);
-  }
-
-  public void setExeFlags(List<String> flags) {
-    this.exeFlags.setFlags(flags);
-  }
-
-  public void setBlazeBinary(@Nullable String blazeBinary) {
-    this.blazeBinary.setBlazeBinary(blazeBinary);
+  public BlazeRunOnDistributedExecutorState getRunOnDistributedExecutorState() {
+    return runOnDistributedExecutor;
   }
 
   /** Searches through all blaze flags for the first one beginning with '--test_filter' */
   @Nullable
   public String getTestFilterFlag() {
-    for (String flag : getBlazeFlags()) {
+    for (String flag : getBlazeFlagsState().getExpandedFlags()) {
       if (flag.startsWith(BlazeFlags.TEST_FILTER)) {
         return flag;
       }
@@ -96,19 +81,11 @@
     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) {
+    if (getCommandState().getCommand() == null) {
       throw new RuntimeConfigurationError("You must specify a command.");
     }
-    String blazeBinaryString = getBlazeBinary();
+    String blazeBinaryString = getBlazeBinaryState().getBlazeBinary();
     if (blazeBinaryString != null && !(new File(blazeBinaryString).exists())) {
       throw new RuntimeConfigurationError(buildSystemName + " binary does not exist");
     }
@@ -135,7 +112,7 @@
 
         // this editor needs to update based on state provided by other children.
         if (runOnExecutorEditor != null) {
-          boolean isTest = BlazeCommandName.TEST.equals(state.getCommand());
+          boolean isTest = BlazeCommandName.TEST.equals(state.getCommandState().getCommand());
           runOnExecutorEditor.updateVisibility(isTest);
         }
       }
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 87d306c..9cb8a3c 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
@@ -17,6 +17,7 @@
 
 import com.google.common.base.Strings;
 import com.google.common.collect.ImmutableList;
+import com.google.idea.blaze.base.command.BlazeFlags;
 import com.google.idea.blaze.base.ui.UiUtil;
 import com.intellij.openapi.project.Project;
 import com.intellij.openapi.util.text.StringUtil;
@@ -42,11 +43,17 @@
     this.fieldLabel = fieldLabel;
   }
 
-  public List<String> getFlags() {
+  /** @return Flags subject to macro expansion. */
+  public List<String> getExpandedFlags() {
+    return BlazeFlags.expandBuildFlags(flags);
+  }
+
+  /** @return Raw flags that haven't been macro expanded */
+  public List<String> getRawFlags() {
     return flags;
   }
 
-  public void setFlags(List<String> flags) {
+  public void setRawFlags(List<String> flags) {
     this.flags = ImmutableList.copyOf(flags);
   }
 
@@ -118,13 +125,13 @@
     @Override
     public void resetEditorFrom(RunConfigurationState genericState) {
       RunConfigurationFlagsState state = (RunConfigurationFlagsState) genericState;
-      flagsField.setText(makeFlagString(state.getFlags()));
+      flagsField.setText(makeFlagString(state.getRawFlags()));
     }
 
     @Override
     public void applyEditorTo(RunConfigurationState genericState) {
       RunConfigurationFlagsState state = (RunConfigurationFlagsState) genericState;
-      state.setFlags(ParametersListUtil.parse(Strings.nullToEmpty(flagsField.getText())));
+      state.setRawFlags(ParametersListUtil.parse(Strings.nullToEmpty(flagsField.getText())));
     }
 
     @Override
diff --git a/base/src/com/google/idea/blaze/base/run/targetfinder/TargetFinder.java b/base/src/com/google/idea/blaze/base/run/targetfinder/TargetFinder.java
index cc88533..23a03a2 100644
--- a/base/src/com/google/idea/blaze/base/run/targetfinder/TargetFinder.java
+++ b/base/src/com/google/idea/blaze/base/run/targetfinder/TargetFinder.java
@@ -35,7 +35,7 @@
 
   @Nullable
   public TargetIdeInfo targetForLabel(Project project, final Label label) {
-    return findTarget(project, target -> target.key.label.equals(label));
+    return findTarget(project, target -> target.key.label.equals(label) && target.isPlainTarget());
   }
 
   public ImmutableList<TargetIdeInfo> targetsOfKinds(Project project, final Kind... kinds) {
diff --git a/base/src/com/google/idea/blaze/base/run/testlogs/BlazeCommandLogParser.java b/base/src/com/google/idea/blaze/base/run/testlogs/BlazeCommandLogParser.java
index 9993c83..6920313 100644
--- a/base/src/com/google/idea/blaze/base/run/testlogs/BlazeCommandLogParser.java
+++ b/base/src/com/google/idea/blaze/base/run/testlogs/BlazeCommandLogParser.java
@@ -36,6 +36,8 @@
   private static final Logger logger = Logger.getInstance(BlazeCommandLogParser.class);
 
   private static final Pattern TEST_LOG = Pattern.compile("^(//[^\\s]*) .*? (PASSED|FAILED)");
+  private static final Pattern FAILED_TARGET =
+      Pattern.compile("^Target (//[^\\s]*) failed to build");
 
   /** Finds log location and target label for all tests listed in the master log. */
   public static ImmutableSet<Label> parseTestTargets(File commandLog) {
@@ -47,6 +49,25 @@
     }
   }
 
+  /** Finds the targets which failed to build */
+  public static ImmutableSet<Label> parseFailedTargets(File commandLog) {
+    try (Stream<String> stream = Files.lines(Paths.get(commandLog.getPath()))) {
+      return parseTestTargets(stream);
+    } catch (IOException e) {
+      logger.warn("Error parsing master log", e);
+      return ImmutableSet.of();
+    }
+  }
+
+  @VisibleForTesting
+  static ImmutableSet<Label> parseFailedTargets(Stream<String> lines) {
+    return ImmutableSet.copyOf(
+        lines
+            .map(BlazeCommandLogParser::parseBuildFailure)
+            .filter(Objects::nonNull)
+            .collect(Collectors.toSet()));
+  }
+
   @VisibleForTesting
   static ImmutableSet<Label> parseTestTargets(Stream<String> lines) {
     return ImmutableSet.copyOf(
@@ -65,4 +86,14 @@
     }
     return Label.createIfValid(match.group(1));
   }
+
+  @Nullable
+  @VisibleForTesting
+  static Label parseBuildFailure(String line) {
+    Matcher match = FAILED_TARGET.matcher(line);
+    if (!match.find()) {
+      return null;
+    }
+    return Label.createIfValid(match.group(1));
+  }
 }
diff --git a/base/src/com/google/idea/blaze/base/run/testlogs/BlazeTestResultFinderStrategy.java b/base/src/com/google/idea/blaze/base/run/testlogs/BlazeTestResultFinderStrategy.java
new file mode 100644
index 0000000..cff8bd4
--- /dev/null
+++ b/base/src/com/google/idea/blaze/base/run/testlogs/BlazeTestResultFinderStrategy.java
@@ -0,0 +1,54 @@
+/*
+ * Copyright 2017 The Bazel Authors. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.idea.blaze.base.run.testlogs;
+
+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 javax.annotation.Nullable;
+
+/** A strategy for locating results from 'blaze test' invocation (e.g. output XML files). */
+public interface BlazeTestResultFinderStrategy {
+
+  ExtensionPointName<BlazeTestResultFinderStrategy> EP_NAME =
+      ExtensionPointName.create("com.google.idea.blaze.BlazeTestXmlFinderStrategy");
+
+  /**
+   * Attempt to find all output test XML files produced by the most recent blaze invocation, grouped
+   * by target label.
+   */
+  @Nullable
+  static BlazeTestResults locateTestResults(Project project) {
+    BuildSystem buildSystem = Blaze.getBuildSystem(project);
+    for (BlazeTestResultFinderStrategy strategy : EP_NAME.getExtensions()) {
+      if (strategy.handlesBuildSystem(buildSystem)) {
+        return strategy.findTestResults(project);
+      }
+    }
+    return null;
+  }
+
+  /**
+   * Attempt to find test results corresponding to the most recent blaze invocation. Called after
+   * the 'blaze test' process completes.
+   */
+  @Nullable
+  BlazeTestResults findTestResults(Project project);
+
+  /** Results are taken from the first strategy handling a given build system */
+  boolean handlesBuildSystem(BuildSystem buildSystem);
+}
diff --git a/base/src/com/google/idea/blaze/base/run/testlogs/BlazeTestResults.java b/base/src/com/google/idea/blaze/base/run/testlogs/BlazeTestResults.java
new file mode 100644
index 0000000..7d50bf2
--- /dev/null
+++ b/base/src/com/google/idea/blaze/base/run/testlogs/BlazeTestResults.java
@@ -0,0 +1,36 @@
+/*
+ * Copyright 2017 The Bazel Authors. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.idea.blaze.base.run.testlogs;
+
+import com.google.common.collect.ImmutableMultimap;
+import com.google.common.collect.ImmutableSet;
+import com.google.idea.blaze.base.model.primitives.Label;
+import java.io.File;
+
+/** Results from a 'blaze test' invocation. */
+public class BlazeTestResults {
+
+  /** Output test XML files, grouped by target label. */
+  public final ImmutableMultimap<Label, File> testXmlFiles;
+  /** Targets which failed to build */
+  public final ImmutableSet<Label> failedTargets;
+
+  public BlazeTestResults(
+      ImmutableMultimap<Label, File> testXmlFiles, ImmutableSet<Label> failedTargets) {
+    this.testXmlFiles = testXmlFiles;
+    this.failedTargets = failedTargets;
+  }
+}
diff --git a/base/src/com/google/idea/blaze/base/run/testlogs/BlazeTestXmlFinderStrategy.java b/base/src/com/google/idea/blaze/base/run/testlogs/BlazeTestXmlFinderStrategy.java
deleted file mode 100644
index 174d512..0000000
--- a/base/src/com/google/idea/blaze/base/run/testlogs/BlazeTestXmlFinderStrategy.java
+++ /dev/null
@@ -1,52 +0,0 @@
-/*
- * Copyright 2017 The Bazel Authors. All rights reserved.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.google.idea.blaze.base.run.testlogs;
-
-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;
-
-/** A strategy for locating output test XML files. */
-public interface BlazeTestXmlFinderStrategy {
-
-  ExtensionPointName<BlazeTestXmlFinderStrategy> EP_NAME =
-      ExtensionPointName.create("com.google.idea.blaze.BlazeTestXmlFinderStrategy");
-
-  /**
-   * Attempt to find all output test XML files associated with the given run configuration. Called
-   * after the 'blaze test' process completes.
-   */
-  static ImmutableList<CompletedTestTarget> locateTestXmlFiles(Project project) {
-    BuildSystem buildSystem = Blaze.getBuildSystem(project);
-    ImmutableList.Builder<CompletedTestTarget> output = ImmutableList.builder();
-    for (BlazeTestXmlFinderStrategy strategy : EP_NAME.getExtensions()) {
-      if (strategy.handlesBuildSystem(buildSystem)) {
-        output.addAll(strategy.findTestXmlFiles(project));
-      }
-    }
-    return output.build();
-  }
-
-  /**
-   * Attempt to find all output test XML files associated with the given run configuration using a
-   * particular strategy. Called after the 'blaze test' process completes.
-   */
-  ImmutableList<CompletedTestTarget> findTestXmlFiles(Project project);
-
-  boolean handlesBuildSystem(BuildSystem buildSystem);
-}
diff --git a/base/src/com/google/idea/blaze/base/run/testlogs/TargetPathTestXmlFinderStrategy.java b/base/src/com/google/idea/blaze/base/run/testlogs/TargetPathTestResultFinderStrategy.java
similarity index 75%
rename from base/src/com/google/idea/blaze/base/run/testlogs/TargetPathTestXmlFinderStrategy.java
rename to base/src/com/google/idea/blaze/base/run/testlogs/TargetPathTestResultFinderStrategy.java
index ce5ec40..c18cc65 100644
--- a/base/src/com/google/idea/blaze/base/run/testlogs/TargetPathTestXmlFinderStrategy.java
+++ b/base/src/com/google/idea/blaze/base/run/testlogs/TargetPathTestResultFinderStrategy.java
@@ -15,7 +15,7 @@
  */
 package com.google.idea.blaze.base.run.testlogs;
 
-import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMultimap;
 import com.google.idea.blaze.base.command.info.BlazeInfo;
 import com.google.idea.blaze.base.model.BlazeProjectData;
 import com.google.idea.blaze.base.model.primitives.Label;
@@ -24,15 +24,13 @@
 import com.google.idea.blaze.base.sync.data.BlazeProjectDataManager;
 import com.intellij.openapi.project.Project;
 import java.io.File;
-import java.util.Objects;
-import java.util.stream.Collectors;
 import javax.annotation.Nullable;
 
 /**
  * Attempts to parse the list of test targets from the command log, then searches the corresponding
  * path in the bazel-testlogs output tree.
  */
-public class TargetPathTestXmlFinderStrategy implements BlazeTestXmlFinderStrategy {
+public class TargetPathTestResultFinderStrategy implements BlazeTestResultFinderStrategy {
 
   @Override
   public boolean handlesBuildSystem(BuildSystem buildSystem) {
@@ -40,28 +38,30 @@
   }
 
   @Override
-  public ImmutableList<CompletedTestTarget> findTestXmlFiles(Project project) {
+  public BlazeTestResults findTestResults(Project project) {
     File testLogsDir = getTestLogsTree(project);
     if (testLogsDir == null) {
-      return ImmutableList.of();
+      return null;
     }
     File commandLog = getCommandLog(project);
     if (commandLog == null) {
-      return ImmutableList.of();
+      return null;
     }
-    return ImmutableList.copyOf(
-        BlazeCommandLogParser.parseTestTargets(commandLog)
-            .stream()
-            .map((label) -> toKindAndTestXml(testLogsDir, label))
-            .filter(Objects::nonNull)
-            .collect(Collectors.toList()));
+    ImmutableMultimap.Builder<Label, File> output = ImmutableMultimap.builder();
+    for (Label label : BlazeCommandLogParser.parseTestTargets(commandLog)) {
+      File testXml = findTestXml(testLogsDir, label);
+      if (testXml != null) {
+        output.put(label, testXml);
+      }
+    }
+    return new BlazeTestResults(
+        output.build(), BlazeCommandLogParser.parseFailedTargets(commandLog));
   }
 
   @Nullable
-  private static CompletedTestTarget toKindAndTestXml(File testLogsDir, Label label) {
+  private static File findTestXml(File testLogsDir, Label label) {
     String labelPath = label.blazePackage() + File.separator + label.targetName();
-    File testXml = new File(testLogsDir, labelPath + File.separator + "test.xml");
-    return new CompletedTestTarget(testXml, label);
+    return new File(testLogsDir, labelPath + File.separator + "test.xml");
   }
 
   @Nullable
diff --git a/base/src/com/google/idea/blaze/base/scope/BlazeContext.java b/base/src/com/google/idea/blaze/base/scope/BlazeContext.java
index 316c05b..f502c10 100644
--- a/base/src/com/google/idea/blaze/base/scope/BlazeContext.java
+++ b/base/src/com/google/idea/blaze/base/scope/BlazeContext.java
@@ -20,8 +20,8 @@
 import com.google.common.collect.ListMultimap;
 import com.google.common.collect.Lists;
 import java.util.List;
+import javax.annotation.Nullable;
 import org.jetbrains.annotations.NotNull;
-import org.jetbrains.annotations.Nullable;
 
 /** Scoped operation context. */
 public class BlazeContext {
diff --git a/base/src/com/google/idea/blaze/base/scope/Scope.java b/base/src/com/google/idea/blaze/base/scope/Scope.java
index 4c0f12a..e288d8a 100644
--- a/base/src/com/google/idea/blaze/base/scope/Scope.java
+++ b/base/src/com/google/idea/blaze/base/scope/Scope.java
@@ -16,8 +16,9 @@
 package com.google.idea.blaze.base.scope;
 
 import com.intellij.openapi.diagnostic.Logger;
+import com.intellij.openapi.progress.ProcessCanceledException;
+import javax.annotation.Nullable;
 import org.jetbrains.annotations.NotNull;
-import org.jetbrains.annotations.Nullable;
 
 /** Helper methods to run scoped functions and operations in a scoped context. */
 public final class Scope {
@@ -34,6 +35,9 @@
     BlazeContext context = new BlazeContext(parentContext);
     try {
       return scopedFunction.execute(context);
+    } catch (ProcessCanceledException e) {
+      context.setCancelled();
+      throw e;
     } catch (RuntimeException e) {
       context.setHasError();
       logger.error(e);
@@ -54,6 +58,9 @@
     BlazeContext context = new BlazeContext(parentContext);
     try {
       scopedOperation.execute(context);
+    } catch (ProcessCanceledException e) {
+      context.setCancelled();
+      throw e;
     } catch (RuntimeException e) {
       context.setHasError();
       logger.error(e);
diff --git a/base/src/com/google/idea/blaze/base/scope/ScopedTask.java b/base/src/com/google/idea/blaze/base/scope/ScopedTask.java
index 725bf2f..877cbe8 100644
--- a/base/src/com/google/idea/blaze/base/scope/ScopedTask.java
+++ b/base/src/com/google/idea/blaze/base/scope/ScopedTask.java
@@ -18,15 +18,15 @@
 import com.google.idea.blaze.base.scope.scopes.ProgressIndicatorScope;
 import com.intellij.openapi.progress.ProgressIndicator;
 import com.intellij.openapi.progress.Progressive;
+import javax.annotation.Nullable;
 import org.jetbrains.annotations.NotNull;
-import org.jetbrains.annotations.Nullable;
 
 /** Wrapper between an IntelliJ Task and a BlazeContext */
 public abstract class ScopedTask implements Progressive {
   @Nullable final BlazeContext parentContext;
 
   public ScopedTask() {
-    this(null /* parentContext */);
+    this(/* parentContext */ null);
   }
 
   public ScopedTask(@Nullable BlazeContext parentContext) {
diff --git a/base/src/com/google/idea/blaze/base/scope/output/IssueOutput.java b/base/src/com/google/idea/blaze/base/scope/output/IssueOutput.java
index e3bc15a..8690be2 100644
--- a/base/src/com/google/idea/blaze/base/scope/output/IssueOutput.java
+++ b/base/src/com/google/idea/blaze/base/scope/output/IssueOutput.java
@@ -19,8 +19,8 @@
 import com.google.idea.blaze.base.scope.Output;
 import com.intellij.pom.Navigatable;
 import java.io.File;
+import javax.annotation.Nullable;
 import org.jetbrains.annotations.NotNull;
-import org.jetbrains.annotations.Nullable;
 
 /** An issue in a blaze operation. */
 public class IssueOutput implements Output {
diff --git a/base/src/com/google/idea/blaze/base/scope/scopes/BlazeConsoleScope.java b/base/src/com/google/idea/blaze/base/scope/scopes/BlazeConsoleScope.java
index 1875f20..7128856 100644
--- a/base/src/com/google/idea/blaze/base/scope/scopes/BlazeConsoleScope.java
+++ b/base/src/com/google/idea/blaze/base/scope/scopes/BlazeConsoleScope.java
@@ -28,8 +28,8 @@
 import com.intellij.openapi.application.ApplicationManager;
 import com.intellij.openapi.progress.ProgressIndicator;
 import com.intellij.openapi.project.Project;
+import javax.annotation.Nullable;
 import org.jetbrains.annotations.NotNull;
-import org.jetbrains.annotations.Nullable;
 
 /** Moves print output to the blaze console. */
 public class BlazeConsoleScope implements BlazeScope {
diff --git a/base/src/com/google/idea/blaze/base/scope/scopes/TimingScope.java b/base/src/com/google/idea/blaze/base/scope/scopes/TimingScope.java
index d3dcad0..bad8849 100644
--- a/base/src/com/google/idea/blaze/base/scope/scopes/TimingScope.java
+++ b/base/src/com/google/idea/blaze/base/scope/scopes/TimingScope.java
@@ -20,8 +20,8 @@
 import com.google.idea.blaze.base.scope.BlazeScope;
 import com.google.idea.blaze.base.scope.output.PrintOutput;
 import java.util.List;
+import javax.annotation.Nullable;
 import org.jetbrains.annotations.NotNull;
-import org.jetbrains.annotations.Nullable;
 
 /** Prints timing information as output. */
 public class TimingScope implements BlazeScope {
diff --git a/base/src/com/google/idea/blaze/base/settings/BlazeImportSettings.java b/base/src/com/google/idea/blaze/base/settings/BlazeImportSettings.java
index 7f0ea60..e19d578 100644
--- a/base/src/com/google/idea/blaze/base/settings/BlazeImportSettings.java
+++ b/base/src/com/google/idea/blaze/base/settings/BlazeImportSettings.java
@@ -16,13 +16,10 @@
 package com.google.idea.blaze.base.settings;
 
 import com.google.idea.blaze.base.settings.Blaze.BuildSystem;
-import com.intellij.util.xmlb.annotations.Tag;
+import java.util.UUID;
 import javax.annotation.Nullable;
 
-// The tag here is for legacy migration support.
-// No longer needed and can be removed with {@link BlazeImportSettingsManagerLegacy}
 /** Project settings that are set at import time. */
-@Tag("BlazeProjectSettings")
 public final class BlazeImportSettings {
 
   private String workspaceRoot = "";
@@ -46,17 +43,22 @@
       String workspaceRoot,
       String projectName,
       String projectDataDirectory,
-      String locationHash,
       String projectViewFile,
       BuildSystem buildSystem) {
     this.workspaceRoot = workspaceRoot;
     this.projectName = projectName;
     this.projectDataDirectory = projectDataDirectory;
-    this.locationHash = locationHash;
+    this.locationHash = createLocationHash(projectName);
     this.projectViewFile = projectViewFile;
     this.buildSystem = buildSystem;
   }
 
+  private static String createLocationHash(String projectName) {
+    String uuid = UUID.randomUUID().toString();
+    uuid = uuid.substring(0, Math.min(uuid.length(), 8));
+    return projectName.replaceAll("[^a-zA-Z0-9]", "") + "-" + uuid;
+  }
+
   @SuppressWarnings("unused")
   public String getWorkspaceRoot() {
     return workspaceRoot;
diff --git a/base/src/com/google/idea/blaze/base/settings/BlazeImportSettingsManager.java b/base/src/com/google/idea/blaze/base/settings/BlazeImportSettingsManager.java
index a45b08e..093b18a 100644
--- a/base/src/com/google/idea/blaze/base/settings/BlazeImportSettingsManager.java
+++ b/base/src/com/google/idea/blaze/base/settings/BlazeImportSettingsManager.java
@@ -21,8 +21,8 @@
 import com.intellij.openapi.components.Storage;
 import com.intellij.openapi.components.StoragePathMacros;
 import com.intellij.openapi.project.Project;
+import javax.annotation.Nullable;
 import org.jetbrains.annotations.NotNull;
-import org.jetbrains.annotations.Nullable;
 
 /** Manages storage for the project's {@link BlazeImportSettings}. */
 @State(name = "BlazeImportSettings", storages = @Storage(file = StoragePathMacros.WORKSPACE_FILE))
@@ -30,11 +30,7 @@
 
   @Nullable private BlazeImportSettings importSettings;
 
-  private Project project;
-
-  public BlazeImportSettingsManager(@NotNull Project project) {
-    this.project = project;
-  }
+  public BlazeImportSettingsManager() {}
 
   public static BlazeImportSettingsManager getInstance(Project project) {
     return ServiceManager.getService(project, BlazeImportSettingsManager.class);
@@ -54,11 +50,6 @@
 
   @Nullable
   public BlazeImportSettings getImportSettings() {
-    if (importSettings == null) {
-      importSettings =
-          BlazeImportSettingsManagerLegacy.getInstance(project).migrateImportSettings();
-    }
-
     return importSettings;
   }
 
diff --git a/base/src/com/google/idea/blaze/base/settings/BlazeImportSettingsManagerLegacy.java b/base/src/com/google/idea/blaze/base/settings/BlazeImportSettingsManagerLegacy.java
deleted file mode 100644
index fec7d1d..0000000
--- a/base/src/com/google/idea/blaze/base/settings/BlazeImportSettingsManagerLegacy.java
+++ /dev/null
@@ -1,115 +0,0 @@
-/*
- * Copyright 2016 The Bazel Authors. All rights reserved.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.google.idea.blaze.base.settings;
-
-import com.google.common.collect.Lists;
-import com.intellij.openapi.components.PersistentStateComponent;
-import com.intellij.openapi.components.ServiceManager;
-import com.intellij.openapi.components.State;
-import com.intellij.openapi.components.Storage;
-import com.intellij.openapi.components.StoragePathMacros;
-import com.intellij.openapi.components.StorageScheme;
-import com.intellij.openapi.project.Project;
-import com.intellij.util.xmlb.annotations.AbstractCollection;
-import java.util.Collection;
-import java.util.List;
-import org.jetbrains.annotations.NotNull;
-import org.jetbrains.annotations.Nullable;
-
-/**
- * Legacy storage for BlazeImportSettings. Introduced in 1.8, can be removed around ~2.2. Removal of
- * this class will cause old projects to stop loading.
- */
-@State(
-  name = "BlazeSettings",
-  storages = {
-    @Storage(file = StoragePathMacros.PROJECT_FILE),
-    @Storage(
-      file = StoragePathMacros.PROJECT_CONFIG_DIR + "/blaze.xml",
-      scheme = StorageScheme.DIRECTORY_BASED
-    )
-  }
-)
-public class BlazeImportSettingsManagerLegacy
-    implements PersistentStateComponent<BlazeImportSettingsManagerLegacy.State> {
-
-  @Nullable private BlazeImportSettings importSettings;
-
-  @NotNull private Project project;
-
-  public BlazeImportSettingsManagerLegacy(@NotNull Project project) {
-    this.project = project;
-  }
-
-  @NotNull
-  public static BlazeImportSettingsManagerLegacy getInstance(@NotNull Project project) {
-    return ServiceManager.getService(project, BlazeImportSettingsManagerLegacy.class);
-  }
-
-  @SuppressWarnings("unchecked")
-  @Nullable
-  @Override
-  public State getState() {
-    if (importSettings == null) {
-      return null;
-    }
-    State state = new State();
-    List<BlazeImportSettings> value = Lists.newArrayList();
-    value.add(importSettings);
-    state.setLinkedExternalProjectsSettings(value);
-    return state;
-  }
-
-  @Override
-  public void loadState(State state) {
-    if (state == null) {
-      importSettings = null;
-      return;
-    }
-
-    Collection<BlazeImportSettings> settings = state.getLinkedExternalProjectsSettings();
-    if (settings != null && !settings.isEmpty()) {
-      importSettings = settings.iterator().next();
-    } else {
-      importSettings = null;
-    }
-  }
-
-  @Nullable
-  BlazeImportSettings migrateImportSettings() {
-    BlazeImportSettings importSettings = this.importSettings;
-    this.importSettings = null;
-    return importSettings;
-  }
-
-  /** State class for the Blaze settings. */
-  static class State {
-
-    private List<BlazeImportSettings> importSettings = Lists.newArrayList();
-
-    @AbstractCollection(
-      surroundWithTag = false,
-      elementTypes = {BlazeImportSettings.class}
-    )
-    public List<BlazeImportSettings> getLinkedExternalProjectsSettings() {
-      return importSettings;
-    }
-
-    public void setLinkedExternalProjectsSettings(List<BlazeImportSettings> settings) {
-      importSettings = settings;
-    }
-  }
-}
diff --git a/base/src/com/google/idea/blaze/base/settings/ui/AddDirectoryToProjectAction.java b/base/src/com/google/idea/blaze/base/settings/ui/AddDirectoryToProjectAction.java
new file mode 100644
index 0000000..b9b1203
--- /dev/null
+++ b/base/src/com/google/idea/blaze/base/settings/ui/AddDirectoryToProjectAction.java
@@ -0,0 +1,248 @@
+/*
+ * Copyright 2017 The Bazel Authors. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.idea.blaze.base.settings.ui;
+
+import static com.google.common.base.Preconditions.checkState;
+import static java.util.stream.Collectors.toCollection;
+
+import com.google.common.collect.ImmutableSet;
+import com.google.idea.blaze.base.actions.BlazeProjectAction;
+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.WorkspacePath;
+import com.google.idea.blaze.base.model.primitives.WorkspaceRoot;
+import com.google.idea.blaze.base.projectview.ProjectViewEdit;
+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.ListSection.Builder;
+import com.google.idea.blaze.base.projectview.section.sections.DirectoryEntry;
+import com.google.idea.blaze.base.projectview.section.sections.DirectorySection;
+import com.google.idea.blaze.base.projectview.section.sections.TargetSection;
+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.google.idea.blaze.base.sync.BlazeSyncManager;
+import com.google.idea.blaze.base.sync.BlazeSyncParams;
+import com.google.idea.blaze.base.sync.data.BlazeProjectDataManager;
+import com.google.idea.blaze.base.sync.projectview.ImportRoots;
+import com.google.idea.blaze.base.sync.projectview.RelatedWorkspacePathFinder;
+import com.google.idea.blaze.base.sync.workspace.WorkspacePathResolver;
+import com.google.idea.blaze.base.ui.WorkspaceFileTextField;
+import com.intellij.icons.AllIcons;
+import com.intellij.openapi.actionSystem.AnActionEvent;
+import com.intellij.openapi.fileChooser.FileChooserDescriptor;
+import com.intellij.openapi.fileChooser.FileChooserDescriptorFactory;
+import com.intellij.openapi.fileChooser.FileTextField;
+import com.intellij.openapi.project.Project;
+import com.intellij.openapi.ui.DialogWrapper;
+import com.intellij.openapi.ui.Messages;
+import com.intellij.openapi.ui.ValidationInfo;
+import com.intellij.openapi.vfs.VirtualFile;
+import com.intellij.ui.components.JBLabel;
+import com.intellij.util.ui.SwingHelper;
+import java.awt.Component;
+import java.awt.Dimension;
+import java.io.File;
+import java.util.LinkedHashSet;
+import java.util.Set;
+import javax.annotation.Nullable;
+import javax.swing.Box;
+import javax.swing.JComponent;
+import javax.swing.JPanel;
+import javax.swing.SwingConstants;
+
+final class AddDirectoryToProjectAction extends BlazeProjectAction {
+
+  private static final String WARNING_TEXT =
+      "This will add all blaze targets below this directory to your project. This could have a "
+          + "large impact on your project build times if the directory contains a lot of code or "
+          + "expensive genrule targets.";
+
+  @Override
+  protected void actionPerformedInBlazeProject(Project project, AnActionEvent e) {
+    BlazeProjectData blazeProjectData =
+        BlazeProjectDataManager.getInstance(project).getBlazeProjectData();
+    if (blazeProjectData == null) {
+      return;
+    }
+
+    new OpenBlazeWorkspaceFileActionDialog(project, blazeProjectData.workspacePathResolver).show();
+  }
+
+  private static class OpenBlazeWorkspaceFileActionDialog extends DialogWrapper {
+
+    static final int PATH_FIELD_WIDTH = 40;
+    final Project project;
+    final WorkspacePathResolver workspacePathResolver;
+    final JPanel component;
+    final FileTextField fileTextField;
+
+    OpenBlazeWorkspaceFileActionDialog(
+        Project project, WorkspacePathResolver workspacePathResolver) {
+      super(project, /* canBeParent */ false, IdeModalityType.PROJECT);
+      this.project = project;
+      this.workspacePathResolver = workspacePathResolver;
+
+      FileChooserDescriptor descriptor =
+          FileChooserDescriptorFactory.createSingleFolderDescriptor();
+      fileTextField =
+          WorkspaceFileTextField.create(
+              workspacePathResolver, descriptor, PATH_FIELD_WIDTH, myDisposable);
+      JBLabel directoryLabel =
+          new JBLabel("Directory:", AllIcons.Modules.SourceFolder, SwingConstants.LEFT);
+      JPanel directoryPanel =
+          SwingHelper.newHorizontalPanel(
+              Component.TOP_ALIGNMENT, directoryLabel, fileTextField.getField());
+
+      JBLabel warning =
+          new JBLabel(
+              "<html>" + WARNING_TEXT + "</html>",
+              AllIcons.General.BalloonWarning,
+              SwingConstants.LEFT);
+      warning.setPreferredSize(new Dimension(800, 100));
+      component =
+          SwingHelper.newLeftAlignedVerticalPanel(
+              directoryPanel, warning, Box.createVerticalGlue());
+
+      setTitle("Add Directory to Project");
+
+      init();
+    }
+
+    @Nullable
+    @Override
+    protected JComponent createCenterPanel() {
+      return component;
+    }
+
+    @Nullable
+    @Override
+    public JComponent getPreferredFocusedComponent() {
+      return fileTextField.getField();
+    }
+
+    @Nullable
+    @Override
+    protected ValidationInfo doValidate() {
+      VirtualFile selectedFile = fileTextField.getSelectedFile();
+      if (selectedFile == null || !selectedFile.exists()) {
+        return new ValidationInfo("File does not exist", fileTextField.getField());
+      } else if (!selectedFile.isDirectory()) {
+        return new ValidationInfo("File is not a directory", fileTextField.getField());
+      }
+
+      WorkspacePath workspacePath =
+          workspacePathResolver.getWorkspacePath(new File(selectedFile.getPath()));
+      if (workspacePath == null) {
+        return new ValidationInfo("File is not in workspace", fileTextField.getField());
+      }
+
+      ProjectViewSet projectViewSet = ProjectViewManager.getInstance(project).getProjectViewSet();
+      checkState(projectViewSet != null);
+
+      BlazeImportSettings importSettings =
+          BlazeImportSettingsManager.getInstance(project).getImportSettings();
+      checkState(importSettings != null);
+
+      ImportRoots importRoots =
+          ImportRoots.builder(
+                  WorkspaceRoot.fromImportSettings(importSettings), importSettings.getBuildSystem())
+              .add(projectViewSet)
+              .build();
+
+      if (importRoots.containsWorkspacePath(workspacePath)) {
+        return new ValidationInfo("This directory is already included in your project");
+      }
+
+      return null;
+    }
+
+    @Override
+    protected void doOKAction() {
+      VirtualFile selectedFile = fileTextField.getSelectedFile();
+      checkState(selectedFile != null);
+      WorkspacePath workspacePath =
+          workspacePathResolver.getWorkspacePath(new File(selectedFile.getPath()));
+      checkState(workspacePath != null);
+
+      ProjectViewSet projectViewSet = ProjectViewManager.getInstance(project).getProjectViewSet();
+      checkState(projectViewSet != null);
+
+      Set<DirectoryEntry> existingDirectories =
+          ImmutableSet.copyOf(projectViewSet.listItems(DirectorySection.KEY));
+      Set<TargetExpression> existingTargets =
+          ImmutableSet.copyOf(projectViewSet.listItems(TargetSection.KEY));
+
+      Set<WorkspacePath> pathsToAdd = new LinkedHashSet<>();
+      pathsToAdd.add(workspacePath);
+      pathsToAdd.addAll(
+          RelatedWorkspacePathFinder.getInstance()
+              .findRelatedWorkspaceDirectories(workspacePathResolver, workspacePath));
+
+      Set<DirectoryEntry> newDirectories =
+          pathsToAdd
+              .stream()
+              .map(DirectoryEntry::include)
+              .filter(entry -> !existingDirectories.contains(entry))
+              .collect(toCollection(LinkedHashSet::new));
+
+      Set<TargetExpression> newTargets =
+          pathsToAdd
+              .stream()
+              .map(TargetExpression::allFromPackageRecursive)
+              .filter(entry -> !existingTargets.contains(entry))
+              .collect(toCollection(LinkedHashSet::new));
+
+      ProjectViewEdit edit =
+          ProjectViewEdit.editLocalProjectView(
+              project,
+              builder -> {
+                ListSection<DirectoryEntry> directories = builder.getLast(DirectorySection.KEY);
+                Builder<DirectoryEntry> directoriesUpdater =
+                    ListSection.update(DirectorySection.KEY, directories);
+                newDirectories.forEach(directoriesUpdater::add);
+                builder.replace(directories, directoriesUpdater);
+
+                ListSection<TargetExpression> targets = builder.getLast(TargetSection.KEY);
+                Builder<TargetExpression> targetsUpdater =
+                    ListSection.update(TargetSection.KEY, targets);
+                newTargets.forEach(targetsUpdater::add);
+                builder.replace(targets, targetsUpdater);
+
+                return true;
+              });
+
+      if (edit == null) {
+        Messages.showErrorDialog(
+            "Could not modify project view. Check for errors in your project view and try again",
+            "Error");
+        return;
+      }
+
+      edit.apply();
+
+      BlazeSyncManager.getInstance(project)
+          .requestProjectSync(
+              new BlazeSyncParams.Builder("Sync", BlazeSyncParams.SyncMode.INCREMENTAL)
+                  .addProjectViewTargets(true)
+                  .addWorkingSet(BlazeUserSettings.getInstance().getExpandSyncToWorkingSet())
+                  .build());
+
+      super.doOKAction();
+    }
+  }
+}
diff --git a/base/src/com/google/idea/blaze/base/settings/ui/JPanelProvidingProject.java b/base/src/com/google/idea/blaze/base/settings/ui/JPanelProvidingProject.java
index 8e763f1..62370d2 100644
--- a/base/src/com/google/idea/blaze/base/settings/ui/JPanelProvidingProject.java
+++ b/base/src/com/google/idea/blaze/base/settings/ui/JPanelProvidingProject.java
@@ -37,6 +37,11 @@
     this.project = project;
   }
 
+  public JPanelProvidingProject(Project project) {
+    super();
+    this.project = project;
+  }
+
   @Nullable
   @Override
   public Object getData(String dataId) {
diff --git a/base/src/com/google/idea/blaze/base/settings/ui/ProjectViewUi.java b/base/src/com/google/idea/blaze/base/settings/ui/ProjectViewUi.java
index 5023368..ae6a917 100644
--- a/base/src/com/google/idea/blaze/base/settings/ui/ProjectViewUi.java
+++ b/base/src/com/google/idea/blaze/base/settings/ui/ProjectViewUi.java
@@ -133,7 +133,7 @@
     return editor;
   }
 
-  public void fillUi(JPanel canvas, int indentLevel) {
+  public void fillUi(JPanel canvas) {
     String tooltip =
         "Enter a project view descriptor file."
             + (Blaze.defaultBuildSystem() == BuildSystem.Blaze
@@ -149,9 +149,9 @@
 
     JBLabel labelsLabel = new JBLabel("Project View");
     labelsLabel.setToolTipText(tooltip);
-    canvas.add(labelsLabel, UiUtil.getFillLineConstraints(indentLevel));
+    canvas.add(labelsLabel, UiUtil.getFillLineConstraints(0));
 
-    canvas.add(projectViewEditor.getComponent(), UiUtil.getFillLineConstraints(indentLevel));
+    canvas.add(projectViewEditor.getComponent(), UiUtil.getFillLineConstraints(0));
 
     useShared = new JCheckBox(USE_SHARED_PROJECT_VIEW);
     useShared.addActionListener(
@@ -162,7 +162,7 @@
           }
           updateTextAreasEnabled();
         });
-    canvas.add(useShared, UiUtil.getFillLineConstraints(indentLevel));
+    canvas.add(useShared, UiUtil.getFillLineConstraints(0));
   }
 
   public void init(
@@ -185,10 +185,7 @@
     }
 
     useShared.setSelected(useSharedProjectView);
-
-    if (sharedProjectViewText == null) {
-      useShared.setEnabled(false);
-    }
+    useShared.setEnabled(sharedProjectViewText != null);
 
     setDummyWorkspacePathResolverProvider(this.workspacePathResolver);
     setProjectViewText(projectViewText);
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 6675f13..c79a9c9 100644
--- a/base/src/com/google/idea/blaze/base/sync/BlazeSyncPlugin.java
+++ b/base/src/com/google/idea/blaze/base/sync/BlazeSyncPlugin.java
@@ -17,6 +17,7 @@
 
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableSet;
+import com.google.idea.blaze.base.command.info.BlazeInfo;
 import com.google.idea.blaze.base.ideinfo.TargetMap;
 import com.google.idea.blaze.base.model.BlazeProjectData;
 import com.google.idea.blaze.base.model.BlazeVersionData;
@@ -30,7 +31,6 @@
 import com.google.idea.blaze.base.sync.libraries.LibrarySource;
 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 com.intellij.openapi.extensions.ExtensionPointName;
@@ -90,6 +90,12 @@
   /** @return The set of supported languages under this workspace type. */
   Set<LanguageClass> getSupportedLanguagesInWorkspace(WorkspaceType workspaceType);
 
+  /**
+   * @return The set of languages which are always active, regardless of which
+   *     'additional_languages' are requested.
+   */
+  Set<LanguageClass> getAlwaysActiveLanguages();
+
   /** Installs any global SDKs */
   void installSdks(BlazeContext context);
 
@@ -100,7 +106,7 @@
       WorkspaceRoot workspaceRoot,
       ProjectViewSet projectViewSet,
       WorkspaceLanguageSettings workspaceLanguageSettings,
-      BlazeRoots blazeRoots,
+      BlazeInfo blazeInfo,
       @Nullable WorkingSet workingSet,
       WorkspacePathResolver workspacePathResolver,
       ArtifactLocationDecoder artifactLocationDecoder,
@@ -160,6 +166,7 @@
    * @return True for success, false for fatal error.
    */
   boolean validateProjectView(
+      @Nullable Project project,
       BlazeContext context,
       ProjectViewSet projectViewSet,
       WorkspaceLanguageSettings workspaceLanguageSettings);
@@ -168,7 +175,7 @@
   Collection<SectionParser> getSections();
 
   @Nullable
-  LibrarySource getLibrarySource(BlazeProjectData blazeProjectData);
+  LibrarySource getLibrarySource(ProjectViewSet projectViewSet, BlazeProjectData blazeProjectData);
 
   /** Convenience adapter to help stubbing out methods. */
   class Adapter implements BlazeSyncPlugin {
@@ -196,6 +203,11 @@
     }
 
     @Override
+    public Set<LanguageClass> getAlwaysActiveLanguages() {
+      return ImmutableSet.of();
+    }
+
+    @Override
     public void installSdks(BlazeContext context) {}
 
     @Override
@@ -205,7 +217,7 @@
         WorkspaceRoot workspaceRoot,
         ProjectViewSet projectViewSet,
         WorkspaceLanguageSettings workspaceLanguageSettings,
-        BlazeRoots blazeRoots,
+        BlazeInfo blazeInfo,
         @Nullable WorkingSet workingSet,
         WorkspacePathResolver workspacePathResolver,
         ArtifactLocationDecoder artifactLocationDecoder,
@@ -259,6 +271,7 @@
 
     @Override
     public boolean validateProjectView(
+        @Nullable Project project,
         BlazeContext context,
         ProjectViewSet projectViewSet,
         WorkspaceLanguageSettings workspaceLanguageSettings) {
@@ -272,7 +285,8 @@
 
     @Nullable
     @Override
-    public LibrarySource getLibrarySource(BlazeProjectData blazeProjectData) {
+    public LibrarySource getLibrarySource(
+        ProjectViewSet projectViewSet, BlazeProjectData blazeProjectData) {
       return null;
     }
   }
diff --git a/base/src/com/google/idea/blaze/base/sync/BlazeSyncTask.java b/base/src/com/google/idea/blaze/base/sync/BlazeSyncTask.java
index 9b6849f..a019ffd 100644
--- a/base/src/com/google/idea/blaze/base/sync/BlazeSyncTask.java
+++ b/base/src/com/google/idea/blaze/base/sync/BlazeSyncTask.java
@@ -16,8 +16,6 @@
 package com.google.idea.blaze.base.sync;
 
 import com.google.common.annotations.VisibleForTesting;
-import com.google.common.collect.ImmutableList;
-import com.google.common.collect.ImmutableMap;
 import com.google.common.collect.ImmutableMultimap;
 import com.google.common.collect.Iterables;
 import com.google.common.collect.Lists;
@@ -26,7 +24,9 @@
 import com.google.common.util.concurrent.ListeningExecutorService;
 import com.google.idea.blaze.base.async.FutureUtil;
 import com.google.idea.blaze.base.async.executor.BlazeExecutor;
+import com.google.idea.blaze.base.command.BlazeFlags;
 import com.google.idea.blaze.base.command.info.BlazeInfo;
+import com.google.idea.blaze.base.command.info.BlazeInfoRunner;
 import com.google.idea.blaze.base.experiments.ExperimentScope;
 import com.google.idea.blaze.base.filecache.FileCaches;
 import com.google.idea.blaze.base.ideinfo.TargetKey;
@@ -65,21 +65,26 @@
 import com.google.idea.blaze.base.sync.BlazeSyncPlugin.ModuleEditor;
 import com.google.idea.blaze.base.sync.SyncListener.SyncResult;
 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.aspects.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;
 import com.google.idea.blaze.base.sync.projectstructure.ContentEntryEditor;
+import com.google.idea.blaze.base.sync.projectstructure.DirectoryStructure;
 import com.google.idea.blaze.base.sync.projectstructure.ModuleEditorImpl;
 import com.google.idea.blaze.base.sync.projectstructure.ModuleEditorProvider;
+import com.google.idea.blaze.base.sync.projectstructure.ModuleFinder;
 import com.google.idea.blaze.base.sync.projectview.ImportRoots;
 import com.google.idea.blaze.base.sync.projectview.LanguageSupport;
 import com.google.idea.blaze.base.sync.projectview.WorkspaceLanguageSettings;
+import com.google.idea.blaze.base.sync.sharding.BlazeBuildTargetSharder;
+import com.google.idea.blaze.base.sync.sharding.BlazeBuildTargetSharder.ShardedTargetsResult;
+import com.google.idea.blaze.base.sync.sharding.ShardedTargetList;
+import com.google.idea.blaze.base.sync.sharding.SuggestEnablingShardingNotification;
 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;
@@ -90,8 +95,8 @@
 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.ProcessCanceledException;
 import com.intellij.openapi.progress.ProgressIndicator;
 import com.intellij.openapi.progress.Progressive;
 import com.intellij.openapi.project.Project;
@@ -101,6 +106,7 @@
 import com.intellij.openapi.util.io.FileUtil;
 import com.intellij.openapi.vfs.VirtualFileManager;
 import java.io.File;
+import java.io.IOException;
 import java.util.Collection;
 import java.util.Collections;
 import java.util.List;
@@ -153,6 +159,20 @@
         });
   }
 
+  @Nullable
+  private BlazeProjectData getOldProjectData(BlazeContext context) {
+    try {
+      return BlazeProjectDataManagerImpl.getImpl(project).loadProjectRoot(importSettings);
+    } catch (IOException e) {
+      logger.info(e);
+      context.output(
+          new StatusOutput(
+              String.format(
+                  "Couldn't load previously cached project data; full sync will be needed")));
+      return null;
+    }
+  }
+
   /** Returns true if sync successfully completed */
   @VisibleForTesting
   boolean syncProject(BlazeContext context) {
@@ -161,10 +181,7 @@
     try {
       SaveUtil.saveAllFiles();
       BlazeProjectData oldBlazeProjectData =
-          syncMode != SyncMode.FULL
-              ? BlazeProjectDataManagerImpl.getImpl(project)
-                  .loadProjectRoot(context, importSettings)
-              : null;
+          syncMode != SyncMode.FULL ? getOldProjectData(context) : null;
       if (oldBlazeProjectData == null) {
         syncMode = SyncMode.FULL;
       }
@@ -183,8 +200,14 @@
         onSyncComplete(project, context, projectViewSet, blazeProjectData, syncMode, syncResult);
       }
     } catch (AssertionError | Exception e) {
-      logger.error(e);
-      IssueOutput.error("Internal error: " + e.getMessage()).submit(context);
+      Throwable rootCause = e;
+      while (rootCause.getCause() != null) {
+        rootCause = rootCause.getCause();
+      }
+      if (!(rootCause instanceof ProcessCanceledException)) {
+        logger.error(e);
+        IssueOutput.error("Internal error: " + e.getMessage()).submit(context);
+      }
     } finally {
       afterSync(project, context, syncMode, syncResult);
     }
@@ -208,16 +231,27 @@
       return SyncResult.FAILURE;
     }
 
-    ListenableFuture<ImmutableMap<String, String>> blazeInfoFuture =
-        BlazeInfo.getInstance()
-            .runBlazeInfo(
-                context, importSettings.getBuildSystem(), workspaceRoot, ImmutableList.of());
-
     ListeningExecutorService executor = BlazeExecutor.getInstance().getExecutor();
+    WorkspacePathResolverAndProjectView workspacePathResolverAndProjectView =
+        computeWorkspacePathResolverAndProjectView(context, vcsHandler, executor);
+    if (workspacePathResolverAndProjectView == null) {
+      return SyncResult.FAILURE;
+    }
+    ProjectViewSet projectViewSet = workspacePathResolverAndProjectView.projectViewSet;
+
+    ListenableFuture<BlazeInfo> blazeInfoFuture =
+        BlazeInfoRunner.getInstance()
+            .runBlazeInfo(
+                context,
+                importSettings.getBuildSystem(),
+                Blaze.getBuildSystemProvider(project).getSyncBinaryPath(),
+                workspaceRoot,
+                BlazeFlags.buildFlags(project, projectViewSet));
+
     ListenableFuture<WorkingSet> workingSetFuture =
         vcsHandler.getWorkingSet(project, context, workspaceRoot, executor);
 
-    ImmutableMap<String, String> blazeInfo =
+    BlazeInfo blazeInfo =
         FutureUtil.waitForFuture(context, blazeInfoFuture)
             .timed(Blaze.buildSystemName(project) + "Info")
             .withProgressMessage(
@@ -228,24 +262,16 @@
     if (blazeInfo == null) {
       return SyncResult.FAILURE;
     }
-    BlazeRoots blazeRoots =
-        BlazeRoots.build(importSettings.getBuildSystem(), workspaceRoot, blazeInfo);
     BlazeVersionData blazeVersionData =
         BlazeVersionData.build(importSettings.getBuildSystem(), workspaceRoot, blazeInfo);
 
-    WorkspacePathResolverAndProjectView workspacePathResolverAndProjectView =
-        computeWorkspacePathResolverAndProjectView(context, blazeRoots, vcsHandler, executor);
-    if (workspacePathResolverAndProjectView == null) {
-      return SyncResult.FAILURE;
-    }
     WorkspacePathResolver workspacePathResolver =
         workspacePathResolverAndProjectView.workspacePathResolver;
     ArtifactLocationDecoder artifactLocationDecoder =
-        new ArtifactLocationDecoderImpl(blazeRoots, workspacePathResolver);
-    ProjectViewSet projectViewSet = workspacePathResolverAndProjectView.projectViewSet;
+        new ArtifactLocationDecoderImpl(blazeInfo, workspacePathResolver);
 
     WorkspaceLanguageSettings workspaceLanguageSettings =
-        LanguageSupport.createWorkspaceLanguageSettings(context, projectViewSet);
+        LanguageSupport.createWorkspaceLanguageSettings(projectViewSet);
     if (workspaceLanguageSettings == null) {
       return SyncResult.FAILURE;
     }
@@ -255,7 +281,7 @@
     }
 
     if (!ProjectViewVerifier.verifyProjectView(
-        context, workspacePathResolver, projectViewSet, workspaceLanguageSettings)) {
+        project, context, workspacePathResolver, projectViewSet, workspaceLanguageSettings)) {
       return SyncResult.FAILURE;
     }
 
@@ -304,6 +330,14 @@
       printTargets(context, syncParams.title, syncParams.targetExpressions);
     }
 
+    ShardedTargetsResult shardedTargetsResult =
+        BlazeBuildTargetSharder.expandAndShardTargets(
+            project, context, workspaceRoot, projectViewSet, workspacePathResolver, targets);
+    if (shardedTargetsResult.buildResult.status == BuildResult.Status.FATAL_ERROR) {
+      return SyncResult.FAILURE;
+    }
+    ShardedTargetList shardedTargets = shardedTargetsResult.shardedTargets;
+
     boolean mergeWithOldState = !syncParams.addProjectViewTargets;
     BlazeIdeInterface.IdeResult ideQueryResult =
         getIdeQueryResult(
@@ -311,7 +345,7 @@
             context,
             projectViewSet,
             blazeVersionData,
-            targets,
+            shardedTargets,
             workspaceLanguageSettings,
             artifactLocationDecoder,
             syncStateBuilder,
@@ -320,8 +354,12 @@
     if (context.isCancelled()) {
       return SyncResult.CANCELLED;
     }
-    if (ideQueryResult.targetMap == null || ideQueryResult.buildResult == BuildResult.FATAL_ERROR) {
+    if (ideQueryResult.targetMap == null
+        || ideQueryResult.buildResult.status == BuildResult.Status.FATAL_ERROR) {
       context.setHasError();
+      if (ideQueryResult.buildResult.outOfMemory()) {
+        SuggestEnablingShardingNotification.suggestSharding(project, context);
+      }
       return SyncResult.FAILURE;
     }
 
@@ -333,9 +371,12 @@
 
     BuildResult ideResolveResult =
         resolveIdeArtifacts(
-            project, context, workspaceRoot, projectViewSet, blazeVersionData, targets);
-    if (ideResolveResult == BuildResult.FATAL_ERROR) {
+            project, context, workspaceRoot, projectViewSet, blazeVersionData, shardedTargets);
+    if (ideResolveResult.status == BuildResult.Status.FATAL_ERROR) {
       context.setHasError();
+      if (ideResolveResult.outOfMemory()) {
+        SuggestEnablingShardingNotification.suggestSharding(project, context);
+      }
       return SyncResult.FAILURE;
     }
     if (context.isCancelled()) {
@@ -353,7 +394,7 @@
                 workspaceRoot,
                 projectViewSet,
                 workspaceLanguageSettings,
-                blazeRoots,
+                blazeInfo,
                 workingSet,
                 workspacePathResolver,
                 artifactLocationDecoder,
@@ -378,45 +419,66 @@
             syncStartTime,
             targetMap,
             blazeInfo,
-            blazeRoots,
             blazeVersionData,
             workspacePathResolver,
             artifactLocationDecoder,
             workspaceLanguageSettings,
             syncStateBuilder.build(),
-            reverseDependencies,
-            null);
+            reverseDependencies);
 
     FileCaches.onSync(project, context, projectViewSet, newBlazeProjectData, syncMode);
     ListenableFuture<?> prefetch =
-        PrefetchService.getInstance().prefetchProjectFiles(project, newBlazeProjectData);
+        PrefetchService.getInstance()
+            .prefetchProjectFiles(project, projectViewSet, newBlazeProjectData);
     FutureUtil.waitForFuture(context, prefetch)
         .withProgressMessage("Prefetching files...")
         .timed("PrefetchFiles")
         .onError("Prefetch failed")
         .run();
 
+    ListenableFuture<DirectoryStructure> directoryStructureFuture =
+        DirectoryStructure.getRootDirectoryStructure(project, workspaceRoot, projectViewSet);
+
     refreshVirtualFileSystem(context, newBlazeProjectData);
 
+    DirectoryStructure directoryStructure =
+        FutureUtil.waitForFuture(context, directoryStructureFuture)
+            .withProgressMessage("Computing directory structure...")
+            .timed("DirectoryStructure")
+            .onError("Directory structure computation failed")
+            .run()
+            .result();
+    if (directoryStructure == null) {
+      return SyncResult.FAILURE;
+    }
+
     boolean success =
         updateProject(
-            context, projectViewSet, blazeVersionData, oldBlazeProjectData, newBlazeProjectData);
+            context,
+            projectViewSet,
+            blazeVersionData,
+            directoryStructure,
+            oldBlazeProjectData,
+            newBlazeProjectData);
     if (!success) {
       return SyncResult.FAILURE;
     }
 
     SyncResult syncResult = SyncResult.SUCCESS;
 
-    if (ideInfoResult == BuildResult.BUILD_ERROR || ideResolveResult == BuildResult.BUILD_ERROR) {
+    if (ideInfoResult.status == BuildResult.Status.BUILD_ERROR
+        || ideResolveResult.status == BuildResult.Status.BUILD_ERROR) {
       final String errorType =
-          ideInfoResult == BuildResult.BUILD_ERROR ? "BUILD file errors" : "compilation errors";
+          ideInfoResult.status == BuildResult.Status.BUILD_ERROR
+              ? "BUILD file errors"
+              : "compilation errors";
 
       String message =
           String.format(
               "Sync was successful, but there were %s. "
                   + "The project may not be fully updated or resolve until fixed. "
                   + "If the errors are from your working set, please uncheck "
-                  + "'Blaze > Expand Sync to Working Set' and try again.",
+                  + "'Blaze > Sync > Expand Sync to Working Set' and try again.",
               errorType);
       context.output(PrintOutput.error(message));
       IssueOutput.warn(message).submit(context);
@@ -457,10 +519,7 @@
   }
 
   private WorkspacePathResolverAndProjectView computeWorkspacePathResolverAndProjectView(
-      BlazeContext context,
-      BlazeRoots blazeRoots,
-      BlazeVcsHandler vcsHandler,
-      ListeningExecutorService executor) {
+      BlazeContext context, BlazeVcsHandler vcsHandler, ListeningExecutorService executor) {
     context.output(new StatusOutput("Updating VCS..."));
 
     for (int i = 0; i < 3; ++i) {
@@ -484,7 +543,7 @@
       WorkspacePathResolver workspacePathResolver =
           vcsWorkspacePathResolver != null
               ? vcsWorkspacePathResolver
-              : new WorkspacePathResolverImpl(workspaceRoot, blazeRoots);
+              : new WorkspacePathResolverImpl(workspaceRoot);
 
       ProjectViewSet projectViewSet =
           ProjectViewManager.getInstance(project).reloadProjectView(context, workspacePathResolver);
@@ -580,7 +639,7 @@
       BlazeContext parentContext,
       ProjectViewSet projectViewSet,
       BlazeVersionData blazeVersionData,
-      List<TargetExpression> targets,
+      ShardedTargetList shardedTargets,
       WorkspaceLanguageSettings workspaceLanguageSettings,
       ArtifactLocationDecoder artifactLocationDecoder,
       Builder syncStateBuilder,
@@ -601,7 +660,7 @@
               workspaceRoot,
               projectViewSet,
               blazeVersionData,
-              targets,
+              shardedTargets,
               workspaceLanguageSettings,
               artifactLocationDecoder,
               syncStateBuilder,
@@ -616,7 +675,7 @@
       WorkspaceRoot workspaceRoot,
       ProjectViewSet projectViewSet,
       BlazeVersionData blazeVersionData,
-      List<TargetExpression> targetExpressions) {
+      ShardedTargetList shardedTargets) {
     return Scope.push(
         parentContext,
         context -> {
@@ -626,12 +685,12 @@
           // We don't want IDE resolve errors to fail the whole sync
           context.setPropagatesErrors(false);
 
-          if (targetExpressions.isEmpty()) {
+          if (shardedTargets.isEmpty()) {
             return BuildResult.SUCCESS;
           }
           BlazeIdeInterface blazeIdeInterface = BlazeIdeInterface.getInstance();
           return blazeIdeInterface.resolveIdeArtifacts(
-              project, context, workspaceRoot, projectViewSet, blazeVersionData, targetExpressions);
+              project, context, workspaceRoot, projectViewSet, blazeVersionData, shardedTargets);
         });
   }
 
@@ -639,6 +698,7 @@
       BlazeContext parentContext,
       ProjectViewSet projectViewSet,
       BlazeVersionData blazeVersionData,
+      DirectoryStructure directoryStructure,
       @Nullable BlazeProjectData oldBlazeProjectData,
       BlazeProjectData newBlazeProjectData) {
     return Scope.push(
@@ -656,19 +716,15 @@
                                 () ->
                                     ProjectRootManagerEx.getInstanceEx(this.project)
                                         .mergeRootsChangesDuring(
-                                            () -> {
-                                              updateProjectSdk(
-                                                  context,
-                                                  projectViewSet,
-                                                  blazeVersionData,
-                                                  newBlazeProjectData);
-                                              updateProjectStructure(
-                                                  context,
-                                                  importSettings,
-                                                  projectViewSet,
-                                                  newBlazeProjectData,
-                                                  oldBlazeProjectData);
-                                            })));
+                                            () ->
+                                                updateProjectStructure(
+                                                    context,
+                                                    importSettings,
+                                                    projectViewSet,
+                                                    blazeVersionData,
+                                                    directoryStructure,
+                                                    newBlazeProjectData,
+                                                    oldBlazeProjectData))));
           } catch (Throwable e) {
             IssueOutput.error("Internal error. Error: " + e).submit(context);
             logger.error(e);
@@ -681,24 +737,20 @@
         });
   }
 
-  private void updateProjectSdk(
-      BlazeContext context,
-      ProjectViewSet projectViewSet,
-      BlazeVersionData blazeVersionData,
-      BlazeProjectData newBlazeProjectData) {
-    for (BlazeSyncPlugin syncPlugin : BlazeSyncPlugin.EP_NAME.getExtensions()) {
-      syncPlugin.updateProjectSdk(
-          project, context, projectViewSet, blazeVersionData, newBlazeProjectData);
-    }
-  }
-
   private void updateProjectStructure(
       BlazeContext context,
       BlazeImportSettings importSettings,
       ProjectViewSet projectViewSet,
+      BlazeVersionData blazeVersionData,
+      DirectoryStructure directoryStructure,
       BlazeProjectData newBlazeProjectData,
       @Nullable BlazeProjectData oldBlazeProjectData) {
 
+    for (BlazeSyncPlugin syncPlugin : BlazeSyncPlugin.EP_NAME.getExtensions()) {
+      syncPlugin.updateProjectSdk(
+          project, context, projectViewSet, blazeVersionData, newBlazeProjectData);
+    }
+
     ModuleEditorImpl moduleEditor =
         ModuleEditorProvider.getInstance().getModuleEditor(project, importSettings);
 
@@ -721,10 +773,17 @@
     ModifiableRootModel workspaceModifiableModel = moduleEditor.editModule(workspaceModule);
 
     ContentEntryEditor.createContentEntries(
-        project, workspaceRoot, projectViewSet, newBlazeProjectData, workspaceModifiableModel);
+        project,
+        workspaceRoot,
+        projectViewSet,
+        newBlazeProjectData,
+        directoryStructure,
+        workspaceModifiableModel);
 
-    List<BlazeLibrary> libraries = BlazeLibraryCollector.getLibraries(newBlazeProjectData);
-    LibraryEditor.updateProjectLibraries(project, context, newBlazeProjectData, libraries);
+    List<BlazeLibrary> libraries =
+        BlazeLibraryCollector.getLibraries(projectViewSet, newBlazeProjectData);
+    LibraryEditor.updateProjectLibraries(
+        project, context, projectViewSet, newBlazeProjectData, libraries);
     LibraryEditor.configureDependencies(workspaceModifiableModel, libraries);
 
     for (BlazeSyncPlugin blazeSyncPlugin : BlazeSyncPlugin.EP_NAME.getExtensions()) {
@@ -760,7 +819,7 @@
               .runReadAction(
                   () -> {
                     Module workspaceModule =
-                        ModuleManager.getInstance(project)
+                        ModuleFinder.getInstance(project)
                             .findModuleByName(BlazeDataStorage.WORKSPACE_MODULE_NAME);
                     for (BlazeSyncPlugin blazeSyncPlugin :
                         BlazeSyncPlugin.EP_NAME.getExtensions()) {
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 c355d5d..34f952a 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
@@ -18,15 +18,14 @@
 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;
 import com.google.idea.blaze.base.projectview.ProjectViewSet;
 import com.google.idea.blaze.base.scope.BlazeContext;
 import com.google.idea.blaze.base.sync.projectview.WorkspaceLanguageSettings;
+import com.google.idea.blaze.base.sync.sharding.ShardedTargetList;
 import com.google.idea.blaze.base.sync.workspace.ArtifactLocationDecoder;
 import com.intellij.openapi.components.ServiceManager;
 import com.intellij.openapi.project.Project;
-import java.util.List;
 import javax.annotation.Nullable;
 
 /** Indirection between ide_build_info and aspect style IDE info. */
@@ -36,22 +35,6 @@
     return ServiceManager.getService(BlazeIdeInterface.class);
   }
 
-  /** The result of a blaze operation */
-  enum BuildResult {
-    SUCCESS, // Success
-    BUILD_ERROR, // Return code 1, a build error
-    FATAL_ERROR; // Some other failure
-
-    public static BuildResult fromExitCode(int exitCode) {
-      if (exitCode == 0) {
-        return SUCCESS;
-      } else if (exitCode == 1) {
-        return BUILD_ERROR;
-      }
-      return FATAL_ERROR;
-    }
-  }
-
   /** The result of the ide operation */
   class IdeResult {
     @Nullable public final TargetMap targetMap;
@@ -75,7 +58,7 @@
       WorkspaceRoot workspaceRoot,
       ProjectViewSet projectViewSet,
       BlazeVersionData blazeVersionData,
-      List<TargetExpression> targets,
+      ShardedTargetList shardedTargets,
       WorkspaceLanguageSettings workspaceLanguageSettings,
       ArtifactLocationDecoder artifactLocationDecoder,
       SyncState.Builder syncStateBuilder,
@@ -93,7 +76,7 @@
       WorkspaceRoot workspaceRoot,
       ProjectViewSet projectViewSet,
       BlazeVersionData blazeVersionData,
-      List<TargetExpression> targets);
+      ShardedTargetList shardedTargets);
 
   /**
    * Attempts to compile the requested ide artifacts.
@@ -106,5 +89,5 @@
       WorkspaceRoot workspaceRoot,
       ProjectViewSet projectViewSet,
       BlazeVersionData blazeVersionData,
-      List<TargetExpression> targets);
+      ShardedTargetList shardedTargets);
 }
diff --git a/base/src/com/google/idea/blaze/base/sync/aspects/BlazeIdeInterfaceAspectsImpl.java b/base/src/com/google/idea/blaze/base/sync/aspects/BlazeIdeInterfaceAspectsImpl.java
index 12a09c5..18a884c 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
@@ -15,10 +15,12 @@
  */
 package com.google.idea.blaze.base.sync.aspects;
 
-import com.google.common.base.Objects;
+import com.google.common.base.Joiner;
 import com.google.common.collect.ImmutableMap;
 import com.google.common.collect.Lists;
 import com.google.common.collect.Maps;
+import com.google.common.collect.Ordering;
+import com.google.common.collect.Sets;
 import com.google.common.util.concurrent.Futures;
 import com.google.common.util.concurrent.ListenableFuture;
 import com.google.common.util.concurrent.ListeningExecutorService;
@@ -26,25 +28,32 @@
 import com.google.idea.blaze.base.async.executor.BlazeExecutor;
 import com.google.idea.blaze.base.async.process.ExternalTask;
 import com.google.idea.blaze.base.async.process.LineProcessingOutputStream;
+import com.google.idea.blaze.base.bazel.BuildSystemProvider;
 import com.google.idea.blaze.base.command.BlazeCommand;
 import com.google.idea.blaze.base.command.BlazeCommandName;
 import com.google.idea.blaze.base.command.BlazeFlags;
-import com.google.idea.blaze.base.command.ExperimentalShowArtifactsLineProcessor;
+import com.google.idea.blaze.base.command.buildresult.BuildResultHelper;
 import com.google.idea.blaze.base.filecache.FileDiffer;
 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.issueparser.IssueOutputLineProcessor;
+import com.google.idea.blaze.base.lang.AdditionalLanguagesHelper;
 import com.google.idea.blaze.base.model.BlazeVersionData;
 import com.google.idea.blaze.base.model.SyncState;
+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.LanguageClass;
 import com.google.idea.blaze.base.model.primitives.TargetExpression;
 import com.google.idea.blaze.base.model.primitives.WorkspaceRoot;
 import com.google.idea.blaze.base.prefetch.PrefetchService;
 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.Result;
 import com.google.idea.blaze.base.scope.Scope;
 import com.google.idea.blaze.base.scope.ScopedFunction;
+import com.google.idea.blaze.base.scope.output.IssueOutput;
 import com.google.idea.blaze.base.scope.output.PerformanceWarning;
 import com.google.idea.blaze.base.scope.output.PrintOutput;
 import com.google.idea.blaze.base.scope.scopes.TimingScope;
@@ -53,20 +62,31 @@
 import com.google.idea.blaze.base.sync.aspects.strategy.AspectStrategy;
 import com.google.idea.blaze.base.sync.aspects.strategy.AspectStrategyProvider;
 import com.google.idea.blaze.base.sync.projectview.WorkspaceLanguageSettings;
+import com.google.idea.blaze.base.sync.sharding.ShardedTargetList;
+import com.google.idea.blaze.base.sync.sharding.WildcardTargetPattern;
 import com.google.idea.blaze.base.sync.workspace.ArtifactLocationDecoder;
 import com.google.repackaged.devtools.intellij.ideinfo.IntellijIdeInfo;
 import com.intellij.openapi.diagnostic.Logger;
 import com.intellij.openapi.project.Project;
+import com.intellij.pom.NavigatableAdapter;
+import com.intellij.util.PathUtil;
 import java.io.File;
 import java.io.FileInputStream;
 import java.io.IOException;
 import java.io.InputStream;
 import java.io.Serializable;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.LinkedHashSet;
 import java.util.List;
 import java.util.Map;
+import java.util.Objects;
+import java.util.Set;
 import java.util.concurrent.ExecutionException;
 import java.util.concurrent.atomic.AtomicLong;
+import java.util.function.Function;
 import java.util.function.Predicate;
+import java.util.stream.Collectors;
 import java.util.zip.GZIPInputStream;
 import javax.annotation.Nullable;
 
@@ -91,7 +111,7 @@
       WorkspaceRoot workspaceRoot,
       ProjectViewSet projectViewSet,
       BlazeVersionData blazeVersionData,
-      List<TargetExpression> targets,
+      ShardedTargetList shardedTargets,
       WorkspaceLanguageSettings workspaceLanguageSettings,
       ArtifactLocationDecoder artifactLocationDecoder,
       SyncState.Builder syncStateBuilder,
@@ -108,22 +128,23 @@
     // If the aspect strategy has changed, redo everything from scratch
     final AspectStrategy aspectStrategy = getAspectStrategy(project, blazeVersionData);
     if (prevState != null
-        && !Objects.equal(prevState.aspectStrategyName, aspectStrategy.getName())) {
+        && !Objects.equals(prevState.aspectStrategyName, aspectStrategy.getName())) {
       prevState = null;
     }
 
     IdeInfoResult ideInfoResult =
-        getIdeInfo(project, context, workspaceRoot, projectViewSet, targets, aspectStrategy);
-    if (ideInfoResult.buildResult == BuildResult.FATAL_ERROR) {
-      return new IdeResult(prevState != null ? prevState.targetMap : null, BuildResult.FATAL_ERROR);
+        getIdeInfo(project, context, workspaceRoot, projectViewSet, shardedTargets, aspectStrategy);
+    if (ideInfoResult.buildResult.status == BuildResult.Status.FATAL_ERROR) {
+      return new IdeResult(
+          prevState != null ? prevState.targetMap : null, ideInfoResult.buildResult);
     }
     // If there was a partial error, make a best-effort attempt to sync. Retain
     // any old state that we have in an attempt not to lose too much code.
-    if (ideInfoResult.buildResult == BuildResult.BUILD_ERROR) {
+    if (ideInfoResult.buildResult.status == BuildResult.Status.BUILD_ERROR) {
       mergeWithOldState = true;
     }
 
-    List<File> fileList = ideInfoResult.files;
+    Collection<File> fileList = ideInfoResult.files;
     List<File> updatedFiles = Lists.newArrayList();
     List<File> removedFiles = Lists.newArrayList();
     ImmutableMap<File, Long> fileState =
@@ -149,13 +170,16 @@
       return new IdeResult(prevState != null ? prevState.targetMap : null, BuildResult.FATAL_ERROR);
     }
 
+    Set<Label> targets = getNonWildcardProjectViewTargets(projectViewSet);
+
     State state =
         updateState(
+            project,
             context,
             prevState,
             fileState,
             workspaceLanguageSettings,
-            artifactLocationDecoder,
+            targets,
             aspectStrategy,
             updatedFiles,
             removedFiles,
@@ -167,11 +191,38 @@
     return new IdeResult(state.targetMap, ideInfoResult.buildResult);
   }
 
-  private static class IdeInfoResult {
-    private final List<File> files;
-    private final BuildResult buildResult;
+  private static Set<Label> getNonWildcardProjectViewTargets(ProjectViewSet projectViewSet) {
+    return projectViewSet
+        .listItems(TargetSection.KEY)
+        .stream()
+        .map(BlazeIdeInterfaceAspectsImpl::singleTargetLabel)
+        .filter(Objects::nonNull)
+        .collect(Collectors.toSet());
+  }
 
-    IdeInfoResult(List<File> files, BuildResult buildResult) {
+  @Nullable
+  private static Label singleTargetLabel(TargetExpression expression) {
+    if (WildcardTargetPattern.fromExpression(expression) != null) {
+      return null;
+    }
+    // convert to a valid Label format
+    String pattern = expression.toString();
+    if (!pattern.startsWith("//")) {
+      pattern = "//" + pattern;
+    }
+    int colonIndex = pattern.indexOf(':');
+    if (colonIndex == -1) {
+      // add the implicit rule name
+      pattern += ":" + PathUtil.getFileName(pattern);
+    }
+    return Label.createIfValid(pattern);
+  }
+
+  private static class IdeInfoResult {
+    final Collection<File> files;
+    final BuildResult buildResult;
+
+    IdeInfoResult(Collection<File> files, BuildResult buildResult) {
       this.files = files;
       this.buildResult = buildResult;
     }
@@ -182,48 +233,70 @@
       BlazeContext parentContext,
       WorkspaceRoot workspaceRoot,
       ProjectViewSet projectViewSet,
-      List<TargetExpression> targets,
+      ShardedTargetList shardedTargets,
       AspectStrategy aspectStrategy) {
     return Scope.push(
         parentContext,
         context -> {
           context.push(
               new TimingScope(String.format("Execute%sCommand", Blaze.buildSystemName(project))));
-
-          List<File> result = Lists.newArrayList();
-
-          BuildSystem buildSystem = Blaze.getBuildSystem(project);
-          BlazeCommand.Builder blazeCommandBuilder =
-              BlazeCommand.builder(buildSystem, BlazeCommandName.BUILD);
-          blazeCommandBuilder.addTargets(targets);
-          blazeCommandBuilder.addBlazeFlags(BlazeFlags.KEEP_GOING);
-          blazeCommandBuilder
-              .addBlazeFlags(BlazeFlags.EXPERIMENTAL_SHOW_ARTIFACTS)
-              .addBlazeFlags(BlazeFlags.buildFlags(project, projectViewSet));
-
-          aspectStrategy.modifyIdeInfoCommand(blazeCommandBuilder);
-
-          String fileExtension = aspectStrategy.getAspectOutputFileExtension();
-          String gzFileExtension = fileExtension + ".gz";
-          Predicate<String> fileFilter =
-              fileName -> fileName.endsWith(fileExtension) || fileName.endsWith(gzFileExtension);
-
-          int retVal =
-              ExternalTask.builder(workspaceRoot)
-                  .addBlazeCommand(blazeCommandBuilder.build())
-                  .context(context)
-                  .stderr(
-                      LineProcessingOutputStream.of(
-                          new ExperimentalShowArtifactsLineProcessor(result, fileFilter),
-                          new IssueOutputLineProcessor(project, context, workspaceRoot)))
-                  .build()
-                  .run();
-
-          BuildResult buildResult = BuildResult.fromExitCode(retVal);
-          return new IdeInfoResult(result, buildResult);
+          Set<File> ideInfoFiles = new LinkedHashSet<>();
+          Function<Integer, String> progressMessage =
+              count ->
+                  String.format(
+                      "Building IDE info files for shard %s of %s...",
+                      count, shardedTargets.shardedTargets.size());
+          Function<List<TargetExpression>, BuildResult> invocation =
+              targets -> {
+                IdeInfoResult result =
+                    getIdeInfoForTargets(
+                        project, context, workspaceRoot, projectViewSet, targets, aspectStrategy);
+                ideInfoFiles.addAll(result.files);
+                return result.buildResult;
+              };
+          BuildResult result =
+              shardedTargets.runShardedCommand(project, context, progressMessage, invocation);
+          return new IdeInfoResult(ideInfoFiles, result);
         });
   }
 
+  /** Runs blaze build with the aspect's ide-info output group for a given set of targets */
+  private static IdeInfoResult getIdeInfoForTargets(
+      Project project,
+      BlazeContext context,
+      WorkspaceRoot workspaceRoot,
+      ProjectViewSet projectViewSet,
+      List<TargetExpression> targets,
+      AspectStrategy aspectStrategy) {
+    String fileExtension = aspectStrategy.getAspectOutputFileExtension();
+    String gzFileExtension = fileExtension + ".gz";
+    Predicate<String> fileFilter =
+        fileName -> fileName.endsWith(fileExtension) || fileName.endsWith(gzFileExtension);
+    BuildResultHelper buildResultHelper = BuildResultHelper.forFiles(fileFilter);
+
+    BlazeCommand.Builder builder =
+        BlazeCommand.builder(getBinaryPath(project), BlazeCommandName.BUILD)
+            .addTargets(targets)
+            .addBlazeFlags(BlazeFlags.KEEP_GOING)
+            .addBlazeFlags(buildResultHelper.getBuildFlags())
+            .addBlazeFlags(BlazeFlags.buildFlags(project, projectViewSet));
+
+    aspectStrategy.modifyIdeInfoCommand(builder);
+
+    int retVal =
+        ExternalTask.builder(workspaceRoot)
+            .addBlazeCommand(builder.build())
+            .context(context)
+            .stderr(
+                buildResultHelper.stderr(
+                    new IssueOutputLineProcessor(project, context, workspaceRoot)))
+            .build()
+            .run();
+
+    BuildResult buildResult = BuildResult.fromExitCode(retVal);
+    return new IdeInfoResult(buildResultHelper.getBuildArtifacts(), buildResult);
+  }
+
   private static class TargetFilePair {
     private final File file;
     private final TargetIdeInfo target;
@@ -236,11 +309,12 @@
 
   @Nullable
   static State updateState(
+      Project project,
       BlazeContext parentContext,
       @Nullable State prevState,
       ImmutableMap<File, Long> fileState,
       WorkspaceLanguageSettings workspaceLanguageSettings,
-      ArtifactLocationDecoder artifactLocationDecoder,
+      Set<Label> nonWildcardProjectTargets,
       AspectStrategy aspectStrategy,
       List<File> newFiles,
       List<File> removedFiles,
@@ -289,6 +363,7 @@
                   }
 
                   AtomicLong totalSizeLoaded = new AtomicLong(0);
+                  Set<LanguageClass> ignoredLanguages = Sets.newConcurrentHashSet();
 
                   ListeningExecutorService executor = BlazeExecutor.getInstance().getExecutor();
 
@@ -300,11 +375,14 @@
                             () -> {
                               totalSizeLoaded.addAndGet(file.length());
                               try (InputStream inputStream = getAspectInputStream(file)) {
-                                IntellijIdeInfo.TargetIdeInfo ruleProto =
+                                IntellijIdeInfo.TargetIdeInfo message =
                                     aspectStrategy.readAspectFile(inputStream);
                                 TargetIdeInfo target =
-                                    IdeInfoFromProtobuf.makeTargetIdeInfo(
-                                        workspaceLanguageSettings, ruleProto);
+                                    protoToTarget(
+                                        workspaceLanguageSettings,
+                                        nonWildcardProjectTargets,
+                                        message,
+                                        ignoredLanguages);
                                 return new TargetFilePair(file, target);
                               }
                             }));
@@ -350,6 +428,8 @@
                                 (100 * duplicateTargetLabels / targetMap.size()))));
                   }
 
+                  warnIgnoredLanguages(project, context, ignoredLanguages);
+
                   state.targetMap = new TargetMap(ImmutableMap.copyOf(targetMap));
                   return Result.of(state);
                 });
@@ -361,6 +441,48 @@
     return result.result;
   }
 
+  @Nullable
+  private static TargetIdeInfo protoToTarget(
+      WorkspaceLanguageSettings languageSettings,
+      Set<Label> nonWildcardProjectTargets,
+      IntellijIdeInfo.TargetIdeInfo message,
+      Set<LanguageClass> ignoredLanguages) {
+    Kind kind = IdeInfoFromProtobuf.getKind(message);
+    if (kind == null) {
+      return null;
+    }
+    if (languageSettings.isLanguageActive(kind.getLanguageClass())) {
+      return IdeInfoFromProtobuf.makeTargetIdeInfo(message);
+    }
+    if (nonWildcardProjectTargets.contains(IdeInfoFromProtobuf.getKey(message).label)) {
+      ignoredLanguages.add(kind.getLanguageClass());
+    }
+    return null;
+  }
+
+  private static void warnIgnoredLanguages(
+      Project project, BlazeContext context, Set<LanguageClass> ignoredLangs) {
+    if (ignoredLangs.isEmpty()) {
+      return;
+    }
+    List<LanguageClass> sorted = new ArrayList<>(ignoredLangs);
+    sorted.sort(Ordering.usingToString());
+
+    String msg =
+        "Some project targets were ignored because the corresponding language support "
+            + "isn't enabled. Click here to enable support for: "
+            + Joiner.on(", ").join(sorted);
+    IssueOutput.warn(msg)
+        .navigatable(
+            new NavigatableAdapter() {
+              @Override
+              public void navigate(boolean requestFocus) {
+                AdditionalLanguagesHelper.enableLanguageSupport(project, sorted);
+              }
+            })
+        .submit(context);
+  }
+
   private static InputStream getAspectInputStream(File file) throws IOException {
     InputStream inputStream = new FileInputStream(file);
     if (file.getName().endsWith(".gz")) {
@@ -376,9 +498,9 @@
       WorkspaceRoot workspaceRoot,
       ProjectViewSet projectViewSet,
       BlazeVersionData blazeVersionData,
-      List<TargetExpression> targets) {
+      ShardedTargetList shardedTargets) {
     return resolveIdeArtifacts(
-        project, context, workspaceRoot, projectViewSet, blazeVersionData, targets, false);
+        project, context, workspaceRoot, projectViewSet, blazeVersionData, shardedTargets, false);
   }
 
   @Override
@@ -388,14 +510,21 @@
       WorkspaceRoot workspaceRoot,
       ProjectViewSet projectViewSet,
       BlazeVersionData blazeVersionData,
-      List<TargetExpression> targets) {
+      ShardedTargetList shardedTargets) {
     boolean ideCompile = hasIdeCompileOutputGroup(blazeVersionData);
     return resolveIdeArtifacts(
-        project, context, workspaceRoot, projectViewSet, blazeVersionData, targets, ideCompile);
+        project,
+        context,
+        workspaceRoot,
+        projectViewSet,
+        blazeVersionData,
+        shardedTargets,
+        ideCompile);
   }
 
   private static boolean hasIdeCompileOutputGroup(BlazeVersionData blazeVersionData) {
-    return blazeVersionData.bazelIsAtLeastVersion(0, 4, 4);
+    return blazeVersionData.buildSystem() == BuildSystem.Blaze
+        || blazeVersionData.bazelIsAtLeastVersion(0, 4, 4);
   }
 
   private static BuildResult resolveIdeArtifacts(
@@ -404,12 +533,39 @@
       WorkspaceRoot workspaceRoot,
       ProjectViewSet projectViewSet,
       BlazeVersionData blazeVersionData,
+      ShardedTargetList shardedTargets,
+      boolean useIdeCompileOutputGroup) {
+
+    Function<Integer, String> progressMessage =
+        count ->
+            String.format(
+                "Building IDE resolve files for shard %s of %s...",
+                count, shardedTargets.shardedTargets.size());
+    Function<List<TargetExpression>, BuildResult> invocation =
+        targets ->
+            doResolveIdeArtifacts(
+                project,
+                context,
+                workspaceRoot,
+                projectViewSet,
+                blazeVersionData,
+                targets,
+                useIdeCompileOutputGroup);
+    return shardedTargets.runShardedCommand(project, context, progressMessage, invocation);
+  }
+
+  private static BuildResult doResolveIdeArtifacts(
+      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)
+        BlazeCommand.builder(getBinaryPath(project), BlazeCommandName.BUILD)
             .addTargets(targets)
             .addBlazeFlags()
             .addBlazeFlags(BlazeFlags.KEEP_GOING)
@@ -447,4 +603,9 @@
     // Should never get here
     throw new IllegalStateException("No aspect strategy found.");
   }
+
+  private static String getBinaryPath(Project project) {
+    BuildSystemProvider buildSystemProvider = Blaze.getBuildSystemProvider(project);
+    return buildSystemProvider.getSyncBinaryPath();
+  }
 }
diff --git a/base/src/com/google/idea/blaze/base/sync/aspects/BuildResult.java b/base/src/com/google/idea/blaze/base/sync/aspects/BuildResult.java
new file mode 100644
index 0000000..d556be7
--- /dev/null
+++ b/base/src/com/google/idea/blaze/base/sync/aspects/BuildResult.java
@@ -0,0 +1,76 @@
+/*
+ * Copyright 2017 The Bazel Authors. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.idea.blaze.base.sync.aspects;
+
+/** The result of a blaze operation */
+public class BuildResult {
+
+  private static final int SUCCESS_EXIT_CODE = 0;
+  private static final int BUILD_ERROR_EXIT_CODE = 1;
+  // blaze server out-of-memory exit code
+  private static final int OOM_EXIT_CODE = 33;
+
+  /** The status of a blaze operation */
+  public enum Status {
+    SUCCESS, // Success
+    BUILD_ERROR, // Return code 1, a build error
+    FATAL_ERROR; // Some other failure
+
+    private static Status fromExitCode(int exitCode) {
+      if (exitCode == SUCCESS_EXIT_CODE) {
+        return SUCCESS;
+      } else if (exitCode == BUILD_ERROR_EXIT_CODE) {
+        return BUILD_ERROR;
+      }
+      return FATAL_ERROR;
+    }
+  }
+
+  public static final BuildResult SUCCESS = fromExitCode(SUCCESS_EXIT_CODE);
+  /** A general fatal error build result */
+  public static final BuildResult FATAL_ERROR = fromExitCode(-1);
+
+  public static BuildResult fromExitCode(int exitCode) {
+    return new BuildResult(exitCode);
+  }
+
+  /** Returns the 'worst' build result of the two. */
+  public static BuildResult combine(BuildResult first, BuildResult second) {
+    return fromExitCode(combineExitCode(first.exitCode, second.exitCode));
+  }
+
+  private final int exitCode;
+  public final Status status;
+
+  private BuildResult(int exitCode) {
+    this.exitCode = exitCode;
+    status = Status.fromExitCode(exitCode);
+  }
+
+  public boolean outOfMemory() {
+    return exitCode == OOM_EXIT_CODE;
+  }
+
+  private static int combineExitCode(int first, int second) {
+    if (first == OOM_EXIT_CODE || second == OOM_EXIT_CODE) {
+      // OOM errors treated specially, so preserve them.
+      return OOM_EXIT_CODE;
+    }
+    Status firstStatus = Status.fromExitCode(first);
+    Status secondStatus = Status.fromExitCode(second);
+    return firstStatus.ordinal() >= secondStatus.ordinal() ? first : second;
+  }
+}
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 8bf9585..a37991f 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
@@ -18,17 +18,18 @@
 
 import static java.util.stream.Collectors.toList;
 
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.base.Joiner;
 import com.google.common.base.Strings;
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.Lists;
 import com.google.idea.blaze.base.ideinfo.AndroidIdeInfo;
+import com.google.idea.blaze.base.ideinfo.AndroidSdkIdeInfo;
 import com.google.idea.blaze.base.ideinfo.ArtifactLocation;
 import com.google.idea.blaze.base.ideinfo.CIdeInfo;
 import com.google.idea.blaze.base.ideinfo.CToolchainIdeInfo;
 import com.google.idea.blaze.base.ideinfo.Dependency;
 import com.google.idea.blaze.base.ideinfo.Dependency.DependencyType;
-import com.google.idea.blaze.base.ideinfo.IntellijPluginDeployInfo;
-import com.google.idea.blaze.base.ideinfo.IntellijPluginDeployInfo.IntellijPluginDeployFile;
 import com.google.idea.blaze.base.ideinfo.JavaIdeInfo;
 import com.google.idea.blaze.base.ideinfo.JavaToolchainIdeInfo;
 import com.google.idea.blaze.base.ideinfo.LibraryArtifact;
@@ -40,8 +41,8 @@
 import com.google.idea.blaze.base.model.primitives.ExecutionRootPath;
 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.projectview.WorkspaceLanguageSettings;
 import com.google.repackaged.devtools.intellij.ideinfo.IntellijIdeInfo;
+import com.intellij.openapi.util.text.StringUtil;
 import java.util.Collection;
 import java.util.List;
 import javax.annotation.Nullable;
@@ -50,23 +51,12 @@
 public class IdeInfoFromProtobuf {
 
   @Nullable
-  public static TargetIdeInfo makeTargetIdeInfo(
-      WorkspaceLanguageSettings workspaceLanguageSettings, IntellijIdeInfo.TargetIdeInfo message) {
+  public static TargetIdeInfo makeTargetIdeInfo(IntellijIdeInfo.TargetIdeInfo message) {
     Kind kind = getKind(message);
     if (kind == null) {
       return null;
     }
-    if (!workspaceLanguageSettings.isLanguageActive(kind.getLanguageClass())) {
-      return null;
-    }
-
-    final TargetKey key;
-    if (message.hasKey()) {
-      key = makeTargetKey(message.getKey());
-    } else {
-      key = TargetKey.forPlainTarget(new Label(message.getLabel()));
-    }
-
+    TargetKey key = getKey(message);
     ArtifactLocation buildFile = getBuildFile(message);
 
     final Collection<Dependency> dependencies;
@@ -107,6 +97,10 @@
     if (message.hasAndroidIdeInfo()) {
       androidIdeInfo = makeAndroidIdeInfo(message.getAndroidIdeInfo());
     }
+    AndroidSdkIdeInfo androidSdkIdeInfo = null;
+    if (message.hasAndroidSdkIdeInfo()) {
+      androidSdkIdeInfo = makeAndroidSdkIdeInfo(message.getAndroidSdkIdeInfo());
+    }
     PyIdeInfo pyIdeInfo = null;
     if (message.hasPyIdeInfo()) {
       pyIdeInfo = makePyIdeInfo(message.getPyIdeInfo());
@@ -125,11 +119,6 @@
     if (message.hasJavaToolchainIdeInfo()) {
       javaToolchainIdeInfo = makeJavaToolchainIdeInfo(message.getJavaToolchainIdeInfo());
     }
-    IntellijPluginDeployInfo intellijPluginDeployInfo = null;
-    if (message.hasIntellijPluginDeployInfo()) {
-      intellijPluginDeployInfo =
-          makeIntellijPluginDeployInfo(message.getIntellijPluginDeployInfo());
-    }
 
     return new TargetIdeInfo(
         key,
@@ -142,23 +131,23 @@
         cToolchainIdeInfo,
         javaIdeInfo,
         androidIdeInfo,
+        androidSdkIdeInfo,
         pyIdeInfo,
         testIdeInfo,
         protoLibraryLegacyInfo,
-        javaToolchainIdeInfo,
-        intellijPluginDeployInfo);
+        javaToolchainIdeInfo);
   }
 
   private static Collection<Dependency> makeDependencyListFromLabelList(
       List<String> dependencyList, Dependency.DependencyType dependencyType) {
     return dependencyList
         .stream()
-        .map(dep -> new Dependency(TargetKey.forPlainTarget(new Label(dep)), dependencyType))
+        .map(dep -> new Dependency(TargetKey.forPlainTarget(Label.create(dep)), dependencyType))
         .collect(toList());
   }
 
   private static TargetKey makeTargetKey(IntellijIdeInfo.TargetKey key) {
-    return TargetKey.forGeneralTarget(new Label(key.getLabel()), key.getAspectIdsList());
+    return TargetKey.forGeneralTarget(Label.create(key.getLabel()), key.getAspectIdsList());
   }
 
   private static Dependency makeDependency(IntellijIdeInfo.Dependency dep) {
@@ -194,10 +183,27 @@
         makeExecutionRootPathList(cIdeInfo.getTransitiveQuoteIncludeDirectoryList());
     List<ExecutionRootPath> transitiveSystemIncludeDirectories =
         makeExecutionRootPathList(cIdeInfo.getTransitiveSystemIncludeDirectoryList());
+    List<String> coptDefines;
+    List<ExecutionRootPath> coptIncludeDirectories;
+    if (cIdeInfo.getTargetCoptList().isEmpty()) {
+      coptDefines = ImmutableList.of();
+      coptIncludeDirectories = ImmutableList.of();
+    } else {
+      UnfilteredCompilerOptions compilerOptions =
+          UnfilteredCompilerOptions.builder()
+              .registerSingleOrSplitOption("-D")
+              .registerSingleOrSplitOption("-I")
+              .build(cIdeInfo.getTargetCoptList());
+      coptDefines = compilerOptions.getExtractedOptionValues("-D");
+      coptIncludeDirectories =
+          makeExecutionRootPathList(compilerOptions.getExtractedOptionValues("-I"));
+    }
 
     CIdeInfo.Builder builder =
         CIdeInfo.builder()
             .addSources(sources)
+            .addLocalDefines(coptDefines)
+            .addLocalIncludeDirectories(coptIncludeDirectories)
             .addTransitiveIncludeDirectories(transitiveIncludeDirectories)
             .addTransitiveQuoteIncludeDirectories(transitiveQuoteIncludeDirectories)
             .addTransitiveDefines(cIdeInfo.getTransitiveDefineList())
@@ -222,8 +228,10 @@
     ExecutionRootPath preprocessorExecutable =
         new ExecutionRootPath(cToolchainIdeInfo.getPreprocessorExecutable());
 
-    UnfilteredCompilerOptions unfilteredCompilerOptions =
-        new UnfilteredCompilerOptions(cToolchainIdeInfo.getUnfilteredCompilerOptionList());
+    UnfilteredCompilerOptions compilerOptions =
+        UnfilteredCompilerOptions.builder()
+            .registerSingleOrSplitOption("-isystem")
+            .build(cToolchainIdeInfo.getUnfilteredCompilerOptionList());
 
     CToolchainIdeInfo.Builder builder =
         CToolchainIdeInfo.builder()
@@ -235,9 +243,9 @@
             .setCppExecutable(cppExecutable)
             .setPreprocessorExecutable(preprocessorExecutable)
             .setTargetName(cToolchainIdeInfo.getTargetName())
-            .addUnfilteredCompilerOptions(unfilteredCompilerOptions.getToolchainFlags())
+            .addUnfilteredCompilerOptions(compilerOptions.getUninterpretedOptions())
             .addUnfilteredToolchainSystemIncludes(
-                unfilteredCompilerOptions.getToolchainSysIncludes());
+                makeExecutionRootPathList(compilerOptions.getExtractedOptionValues("-isystem")));
 
     return builder.build();
   }
@@ -268,10 +276,15 @@
             : null,
         androidIdeInfo.getHasIdlSources(),
         !Strings.isNullOrEmpty(androidIdeInfo.getLegacyResources())
-            ? new Label(androidIdeInfo.getLegacyResources())
+            ? Label.create(androidIdeInfo.getLegacyResources())
             : null);
   }
 
+  private static AndroidSdkIdeInfo makeAndroidSdkIdeInfo(
+      IntellijIdeInfo.AndroidSdkIdeInfo androidSdkIdeInfo) {
+    return new AndroidSdkIdeInfo(makeArtifactLocation(androidSdkIdeInfo.getAndroidJar()));
+  }
+
   private static PyIdeInfo makePyIdeInfo(IntellijIdeInfo.PyIdeInfo info) {
     return PyIdeInfo.builder().addSources(makeArtifactLocationList(info.getSourcesList())).build();
   }
@@ -334,21 +347,6 @@
         javaToolchainIdeInfo.getSourceVersion(), javaToolchainIdeInfo.getTargetVersion());
   }
 
-  private static IntellijPluginDeployInfo makeIntellijPluginDeployInfo(
-      IntellijIdeInfo.IntellijPluginDeployInfo intellijPluginDeployInfo) {
-    return new IntellijPluginDeployInfo(
-        ImmutableList.copyOf(
-            intellijPluginDeployInfo
-                .getDeployFilesList()
-                .stream()
-                .map(
-                    deployFile ->
-                        new IntellijPluginDeployFile(
-                            makeArtifactLocation(deployFile.getSrc()),
-                            deployFile.getDeployLocation()))
-                .collect(toList())));
-  }
-
   private static Collection<LibraryArtifact> makeLibraryArtifactList(
       List<IntellijIdeInfo.LibraryArtifact> jarsList) {
     ImmutableList.Builder<LibraryArtifact> builder = ImmutableList.builder();
@@ -394,26 +392,46 @@
     return builder.build();
   }
 
+  @VisibleForTesting
   @Nullable
-  private static ArtifactLocation makeArtifactLocation(
-      IntellijIdeInfo.ArtifactLocation pbArtifactLocation) {
-    if (pbArtifactLocation == null) {
+  public static ArtifactLocation makeArtifactLocation(
+      @Nullable IntellijIdeInfo.ArtifactLocation location) {
+    if (location == null) {
       return null;
     }
+    String relativePath = location.getRelativePath();
+    String rootExecutionPathFragment = location.getRootExecutionPathFragment();
+    if (!location.getIsNewExternalVersion() && location.getIsExternal()) {
+      // fix up incorrect paths created with older aspect version
+      // Note: bazel always uses the '/' separator here, even on windows.
+      List<String> components = StringUtil.split(relativePath, "/");
+      if (components.size() > 2) {
+        relativePath = Joiner.on('/').join(components.subList(2, components.size()));
+        String prefix = components.get(0) + "/" + components.get(1);
+        rootExecutionPathFragment =
+            rootExecutionPathFragment.isEmpty() ? prefix : rootExecutionPathFragment + "/" + prefix;
+      }
+    }
     return ArtifactLocation.builder()
-        .setRootExecutionPathFragment(pbArtifactLocation.getRootExecutionPathFragment())
-        .setRelativePath(pbArtifactLocation.getRelativePath())
-        .setIsSource(pbArtifactLocation.getIsSource())
-        .setIsExternal(pbArtifactLocation.getIsExternal())
+        .setRootExecutionPathFragment(rootExecutionPathFragment)
+        .setRelativePath(relativePath)
+        .setIsSource(location.getIsSource())
+        .setIsExternal(location.getIsExternal())
         .build();
   }
 
   @Nullable
-  private static Kind getKind(IntellijIdeInfo.TargetIdeInfo target) {
-    String kindString = target.getKindString();
+  static Kind getKind(IntellijIdeInfo.TargetIdeInfo message) {
+    String kindString = message.getKindString();
     if (!Strings.isNullOrEmpty(kindString)) {
       return Kind.fromString(kindString);
     }
     return null;
   }
+
+  static TargetKey getKey(IntellijIdeInfo.TargetIdeInfo message) {
+    return message.hasKey()
+        ? makeTargetKey(message.getKey())
+        : TargetKey.forPlainTarget(Label.create(message.getLabel()));
+  }
 }
diff --git a/base/src/com/google/idea/blaze/base/sync/aspects/UnfilteredCompilerOptions.java b/base/src/com/google/idea/blaze/base/sync/aspects/UnfilteredCompilerOptions.java
index 8509647..e76ead1 100644
--- a/base/src/com/google/idea/blaze/base/sync/aspects/UnfilteredCompilerOptions.java
+++ b/base/src/com/google/idea/blaze/base/sync/aspects/UnfilteredCompilerOptions.java
@@ -15,70 +15,173 @@
  */
 package com.google.idea.blaze.base.sync.aspects;
 
-import com.google.common.annotations.VisibleForTesting;
-import com.google.common.collect.Lists;
-import com.google.idea.blaze.base.model.primitives.ExecutionRootPath;
+import com.google.common.base.Preconditions;
+import com.google.common.collect.ImmutableMap;
+import java.util.ArrayList;
+import java.util.Collection;
 import java.util.List;
 
 /**
- * unfilteredCompilerOptions is a grab bag of options passed to the compiler. Do minimal parsing to
- * extract what we need.
+ * Parses any compiler options that were not extracted by the build system earlier. Do minimal
+ * parsing to extract what we need.
  */
 final class UnfilteredCompilerOptions {
-  private enum NextOption {
-    ISYSTEM,
-    FLAG
-  }
 
-  private final List<ExecutionRootPath> toolchainSysIncludes;
-  private final List<String> toolchainFlags;
+  static class Builder {
+    private final BaseOptionParser baseOptionParser = new BaseOptionParser();
+    private final ImmutableMap.Builder<String, OptionParser> registeredParsers =
+        ImmutableMap.builder();
 
-  public UnfilteredCompilerOptions(Iterable<String> unfilteredOptions) {
-    List<String> toolchainSystemIncludePaths = Lists.newArrayList();
-    toolchainFlags = Lists.newArrayList();
-    splitUnfilteredCompilerOptions(unfilteredOptions, toolchainSystemIncludePaths, toolchainFlags);
+    /** Have the options parser handle the given one-or-two-token option (e.g., -Ifoo or -I foo). */
+    Builder registerSingleOrSplitOption(String optionName) {
+      registeredParsers.put(
+          optionName, new SingleOrSplitOptionParser(optionName, baseOptionParser));
+      return this;
+    }
 
-    toolchainSysIncludes = Lists.newArrayList();
-    for (String systemInclude : toolchainSystemIncludePaths) {
-      toolchainSysIncludes.add(new ExecutionRootPath(systemInclude));
+    /** Parse the given options and build extracted compiler options */
+    UnfilteredCompilerOptions build(Iterable<String> unfilteredOptions) {
+      ImmutableMap<String, OptionParser> registered = registeredParsers.build();
+      baseOptionParser.setRegisteredOptionParsers(registered.values());
+      UnfilteredCompilerOptions options =
+          new UnfilteredCompilerOptions(baseOptionParser, registered);
+      options.parse(unfilteredOptions);
+      return options;
     }
   }
 
-  public List<String> getToolchainFlags() {
-    return toolchainFlags;
+  /** Make a new builder to register options to extract. */
+  static Builder builder() {
+    return new Builder();
   }
 
-  public List<ExecutionRootPath> getToolchainSysIncludes() {
-    return toolchainSysIncludes;
+  private final OptionParser baseOptionParser;
+  private final ImmutableMap<String, OptionParser> registeredParsers;
+
+  private UnfilteredCompilerOptions(
+      OptionParser baseOptionParser, ImmutableMap<String, OptionParser> registeredParsers) {
+    this.baseOptionParser = baseOptionParser;
+    this.registeredParsers = registeredParsers;
   }
 
-  @VisibleForTesting
-  static void splitUnfilteredCompilerOptions(
-      Iterable<String> unfilteredOptions,
-      List<String> toolchainSysIncludes,
-      List<String> toolchainFlags) {
-    NextOption nextOption = NextOption.FLAG;
+  private void parse(Iterable<String> unfilteredOptions) {
+    OptionParser nextOptionParser = baseOptionParser;
     for (String unfilteredOption : unfilteredOptions) {
-      // We are looking for either the flag pair
-      // "-isystem /path/to/dir" or the flag "-isystem/path/to/dir"
-      //
-      // blaze emits isystem flags in both formats.
-      // The latter isn't ideal but apparently it is accepted by GCC and will be emitted by
-      // blaze under certain circumstances.
-      if (nextOption == NextOption.ISYSTEM) {
-        toolchainSysIncludes.add(unfilteredOption);
-        nextOption = NextOption.FLAG;
-      } else {
-        if (unfilteredOption.equals("-isystem")) {
-          nextOption = NextOption.ISYSTEM;
-        } else if (unfilteredOption.startsWith("-isystem")) {
-          String iSystemIncludePath = unfilteredOption.substring("-isystem".length());
-          toolchainSysIncludes.add(iSystemIncludePath);
-        } else {
-          toolchainFlags.add(unfilteredOption);
-          nextOption = NextOption.FLAG;
+      nextOptionParser = nextOptionParser.parseValue(unfilteredOption);
+    }
+  }
+
+  /**
+   * Return the list of arguments that are not extracted (don't correspond to a registered option),
+   * in the original order.
+   */
+  List<String> getUninterpretedOptions() {
+    return baseOptionParser.values();
+  }
+
+  /**
+   * Return the extracted option values for the given registered option name. E.g., if -I is
+   * registered, and ["-foo", "-Ibar"] is parsed then getExtractedOptionValues("-I") returns
+   * ["bar"]. List is in the original order.
+   *
+   * @param optionName the name of a flag that was registered to be extracted
+   * @return option values corresponding to the flag.
+   */
+  List<String> getExtractedOptionValues(String optionName) {
+    OptionParser parser = registeredParsers.get(optionName);
+    Preconditions.checkNotNull(parser);
+    return parser.values();
+  }
+
+  private interface OptionParser {
+    /** Checks if the parser handles the next option value. */
+    boolean handlesOptionValue(String optionValue);
+
+    /**
+     * Parses the option and returns the next handler (assumes {@link #handlesOptionValue} is true).
+     */
+    OptionParser parseValue(String optionValue);
+
+    /** Return a list of option values captured by the parser. */
+    List<String> values();
+  }
+
+  /**
+   * A base option parser that defers to a list of more-specific registered flag parsers, before
+   * handling the flag itself.
+   */
+  private static class BaseOptionParser implements OptionParser {
+    private final List<String> values = new ArrayList<>();
+    private Collection<OptionParser> registeredOptionParsers;
+
+    void setRegisteredOptionParsers(Collection<OptionParser> registeredOptionParsers) {
+      this.registeredOptionParsers = registeredOptionParsers;
+    }
+
+    @Override
+    public boolean handlesOptionValue(String optionValue) {
+      return true;
+    }
+
+    @Override
+    public OptionParser parseValue(String optionValue) {
+      for (OptionParser registeredParser : registeredOptionParsers) {
+        if (registeredParser.handlesOptionValue(optionValue)) {
+          return registeredParser.parseValue(optionValue);
         }
       }
+      values.add(optionValue);
+      return this;
+    }
+
+    @Override
+    public List<String> values() {
+      return values;
+    }
+  }
+
+  /**
+   * A parser that handles flags that can be one or two tokens (e.g., "-Ihdrs", vs "-I", "hdrs").
+   */
+  private static class SingleOrSplitOptionParser implements OptionParser {
+    private final String optionName;
+    private final BaseOptionParser baseOptionParser;
+    private final List<String> values = new ArrayList<>();
+    private boolean consumeNext;
+
+    SingleOrSplitOptionParser(String optionName, BaseOptionParser baseOptionParser) {
+      this.optionName = optionName;
+      this.baseOptionParser = baseOptionParser;
+    }
+
+    @Override
+    public boolean handlesOptionValue(String optionValue) {
+      return consumeNext || optionValue.startsWith(optionName);
+    }
+
+    @Override
+    public OptionParser parseValue(String optionValue) {
+      if (consumeNext) {
+        consumeNext = false;
+        values.add(optionValue);
+        return baseOptionParser;
+      }
+      if (optionValue.equals(optionName)) {
+        consumeNext = true;
+        return this;
+      }
+      if (optionValue.startsWith(optionName)) {
+        values.add(optionValue.substring(optionName.length()));
+        return baseOptionParser;
+      }
+      Preconditions.checkState(
+          false, "Should check handlesOptionValue before attempting to parseValue");
+      return null;
+    }
+
+    @Override
+    public List<String> values() {
+      return values;
     }
   }
 }
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
deleted file mode 100644
index 63787db..0000000
--- a/base/src/com/google/idea/blaze/base/sync/aspects/strategy/AspectStrategyNative.java
+++ /dev/null
@@ -1,61 +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.sync.aspects.strategy;
-
-import com.google.idea.blaze.base.command.BlazeCommand;
-import com.google.repackaged.devtools.intellij.ideinfo.IntellijIdeInfo;
-import java.io.IOException;
-import java.io.InputStream;
-
-/** Aspect strategy for native. */
-public class AspectStrategyNative implements AspectStrategy {
-
-  @Override
-  public String getName() {
-    return "NativeAspect";
-  }
-
-  @Override
-  public void modifyIdeInfoCommand(BlazeCommand.Builder blazeCommandBuilder) {
-    blazeCommandBuilder
-        .addBlazeFlags("--aspects=AndroidStudioInfoAspect")
-        .addBlazeFlags("--output_groups=ide-info");
-  }
-
-  @Override
-  public void modifyIdeResolveCommand(BlazeCommand.Builder blazeCommandBuilder) {
-    blazeCommandBuilder
-        .addBlazeFlags("--aspects=AndroidStudioInfoAspect")
-        .addBlazeFlags("--output_groups=ide-resolve");
-  }
-
-  @Override
-  public void modifyIdeCompileCommand(BlazeCommand.Builder blazeCommandBuilder) {
-    blazeCommandBuilder
-        .addBlazeFlags("--aspects=AndroidStudioInfoAspect")
-        .addBlazeFlags("--output_groups=ide-compile");
-  }
-
-  @Override
-  public String getAspectOutputFileExtension() {
-    return ".aswb-build";
-  }
-
-  @Override
-  public IntellijIdeInfo.TargetIdeInfo readAspectFile(InputStream inputStream) throws IOException {
-    return IntellijIdeInfo.TargetIdeInfo.parseFrom(inputStream);
-  }
-}
diff --git a/base/src/com/google/idea/blaze/base/sync/aspects/strategy/AspectStrategyProviderBazel.java b/base/src/com/google/idea/blaze/base/sync/aspects/strategy/AspectStrategyProviderBazel.java
index 180ae9e..9c0faa4 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
@@ -16,18 +16,11 @@
 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 {
-  private static final BoolExperiment useSkylarkAspect =
-      new BoolExperiment("use.skylark.aspect.bazel.2", true);
-
   @Override
   public AspectStrategy getAspectStrategy(Project project, BlazeVersionData blazeVersionData) {
-    boolean canUseSkylark =
-        useSkylarkAspect.getValue() && blazeVersionData.bazelIsAtLeastVersion(0, 4, 4);
-
-    return canUseSkylark ? new AspectStrategySkylark() : new AspectStrategyNative();
+    return new AspectStrategySkylark();
   }
 }
diff --git a/base/src/com/google/idea/blaze/base/sync/data/BlazeProjectDataManagerImpl.java b/base/src/com/google/idea/blaze/base/sync/data/BlazeProjectDataManagerImpl.java
index bd4494f..2720611 100644
--- a/base/src/com/google/idea/blaze/base/sync/data/BlazeProjectDataManagerImpl.java
+++ b/base/src/com/google/idea/blaze/base/sync/data/BlazeProjectDataManagerImpl.java
@@ -18,8 +18,6 @@
 import com.google.common.collect.Lists;
 import com.google.idea.blaze.base.async.executor.BlazeExecutor;
 import com.google.idea.blaze.base.model.BlazeProjectData;
-import com.google.idea.blaze.base.scope.BlazeContext;
-import com.google.idea.blaze.base.scope.output.StatusOutput;
 import com.google.idea.blaze.base.settings.BlazeImportSettings;
 import com.google.idea.blaze.base.sync.BlazeSyncPlugin;
 import com.google.idea.blaze.base.util.SerializationUtil;
@@ -52,15 +50,14 @@
   }
 
   @Nullable
-  public BlazeProjectData loadProjectRoot(
-      BlazeContext context, BlazeImportSettings importSettings) {
+  public BlazeProjectData loadProjectRoot(BlazeImportSettings importSettings) throws IOException {
     BlazeProjectData projectData = blazeProjectData;
     if (projectData != null) {
       return projectData;
     }
     synchronized (this) {
       projectData = blazeProjectData;
-      return projectData != null ? projectData : loadProject(context, importSettings);
+      return projectData != null ? projectData : loadProject(importSettings);
     }
   }
 
@@ -71,29 +68,18 @@
   }
 
   @Nullable
-  private synchronized BlazeProjectData loadProject(
-      BlazeContext context, BlazeImportSettings importSettings) {
-    BlazeProjectData blazeProjectData = null;
-    try {
-      File file = getCacheFile(project, importSettings);
+  private synchronized BlazeProjectData loadProject(BlazeImportSettings importSettings)
+      throws IOException {
+    File file = getCacheFile(project, importSettings);
 
-      List<ClassLoader> classLoaders = Lists.newArrayList();
-      for (BlazeSyncPlugin syncPlugin : BlazeSyncPlugin.EP_NAME.getExtensions()) {
-        classLoaders.add(syncPlugin.getClass().getClassLoader());
-      }
-      classLoaders.add(getClass().getClassLoader());
-      classLoaders.add(Thread.currentThread().getContextClassLoader());
-
-      blazeProjectData = (BlazeProjectData) SerializationUtil.loadFromDisk(file, classLoaders);
-    } catch (IOException e) {
-      String buildSystemName = importSettings.getBuildSystem().getLowerCaseName();
-      context.output(
-          new StatusOutput(
-              String.format("Stale %s project cache, sync will be needed", buildSystemName)));
-      logger.info(e);
+    List<ClassLoader> classLoaders = Lists.newArrayList();
+    for (BlazeSyncPlugin syncPlugin : BlazeSyncPlugin.EP_NAME.getExtensions()) {
+      classLoaders.add(syncPlugin.getClass().getClassLoader());
     }
+    classLoaders.add(getClass().getClassLoader());
+    classLoaders.add(Thread.currentThread().getContextClassLoader());
 
-    this.blazeProjectData = blazeProjectData;
+    blazeProjectData = (BlazeProjectData) SerializationUtil.loadFromDisk(file, classLoaders);
     return blazeProjectData;
   }
 
diff --git a/base/src/com/google/idea/blaze/base/sync/libraries/BlazeLibraryCollector.java b/base/src/com/google/idea/blaze/base/sync/libraries/BlazeLibraryCollector.java
index 9e029ed..c8e38db 100644
--- a/base/src/com/google/idea/blaze/base/sync/libraries/BlazeLibraryCollector.java
+++ b/base/src/com/google/idea/blaze/base/sync/libraries/BlazeLibraryCollector.java
@@ -16,21 +16,26 @@
 package com.google.idea.blaze.base.sync.libraries;
 
 import com.google.common.collect.Lists;
+import com.google.common.collect.Sets;
 import com.google.idea.blaze.base.model.BlazeLibrary;
 import com.google.idea.blaze.base.model.BlazeProjectData;
+import com.google.idea.blaze.base.projectview.ProjectViewSet;
 import com.google.idea.blaze.base.sync.BlazeSyncPlugin;
 import java.util.List;
 import java.util.Objects;
+import java.util.Set;
 import java.util.function.Predicate;
 import java.util.stream.Collectors;
 
 /** Collects libraries from the sync data using all contributors. */
 public class BlazeLibraryCollector {
-  public static List<BlazeLibrary> getLibraries(BlazeProjectData blazeProjectData) {
-    List<BlazeLibrary> result = Lists.newArrayList();
+  public static List<BlazeLibrary> getLibraries(
+      ProjectViewSet projectViewSet, BlazeProjectData blazeProjectData) {
+    // Use set to filter out duplicates.
+    Set<BlazeLibrary> result = Sets.newLinkedHashSet();
     List<LibrarySource> librarySources = Lists.newArrayList();
     for (BlazeSyncPlugin syncPlugin : BlazeSyncPlugin.EP_NAME.getExtensions()) {
-      LibrarySource librarySource = syncPlugin.getLibrarySource(blazeProjectData);
+      LibrarySource librarySource = syncPlugin.getLibrarySource(projectViewSet, blazeProjectData);
       if (librarySource != null) {
         librarySources.add(librarySource);
       }
diff --git a/base/src/com/google/idea/blaze/base/sync/libraries/LibraryEditor.java b/base/src/com/google/idea/blaze/base/sync/libraries/LibraryEditor.java
index 44969c3..cd59adb 100644
--- a/base/src/com/google/idea/blaze/base/sync/libraries/LibraryEditor.java
+++ b/base/src/com/google/idea/blaze/base/sync/libraries/LibraryEditor.java
@@ -20,6 +20,7 @@
 import com.google.idea.blaze.base.model.BlazeLibrary;
 import com.google.idea.blaze.base.model.BlazeProjectData;
 import com.google.idea.blaze.base.model.LibraryKey;
+import com.google.idea.blaze.base.projectview.ProjectViewSet;
 import com.google.idea.blaze.base.scope.BlazeContext;
 import com.google.idea.blaze.base.scope.output.PrintOutput;
 import com.google.idea.blaze.base.sync.BlazeSyncPlugin;
@@ -45,6 +46,7 @@
   public static void updateProjectLibraries(
       Project project,
       BlazeContext context,
+      ProjectViewSet projectViewSet,
       BlazeProjectData blazeProjectData,
       Collection<BlazeLibrary> libraries) {
     Set<LibraryKey> intelliJLibraryState = Sets.newHashSet();
@@ -71,7 +73,7 @@
       // Garbage collect unused libraries
       List<LibrarySource> librarySources = Lists.newArrayList();
       for (BlazeSyncPlugin syncPlugin : BlazeSyncPlugin.EP_NAME.getExtensions()) {
-        LibrarySource librarySource = syncPlugin.getLibrarySource(blazeProjectData);
+        LibrarySource librarySource = syncPlugin.getLibrarySource(projectViewSet, blazeProjectData);
         if (librarySource != null) {
           librarySources.add(librarySource);
         }
diff --git a/base/src/com/google/idea/blaze/base/sync/projectstructure/ContentEntryEditor.java b/base/src/com/google/idea/blaze/base/sync/projectstructure/ContentEntryEditor.java
index 5b76a0c..b179a7d 100644
--- a/base/src/com/google/idea/blaze/base/sync/projectstructure/ContentEntryEditor.java
+++ b/base/src/com/google/idea/blaze/base/sync/projectstructure/ContentEntryEditor.java
@@ -19,7 +19,6 @@
 import com.google.common.collect.ImmutableMap;
 import com.google.common.collect.Lists;
 import com.google.common.collect.Multimap;
-import com.google.idea.blaze.base.io.FileAttributeProvider;
 import com.google.idea.blaze.base.model.BlazeProjectData;
 import com.google.idea.blaze.base.model.primitives.WorkspacePath;
 import com.google.idea.blaze.base.model.primitives.WorkspaceRoot;
@@ -36,6 +35,7 @@
 import java.io.File;
 import java.util.Collection;
 import java.util.List;
+import java.util.Map;
 
 /** Modifies content entries based on project data. */
 public class ContentEntryEditor {
@@ -45,6 +45,7 @@
       WorkspaceRoot workspaceRoot,
       ProjectViewSet projectViewSet,
       BlazeProjectData blazeProjectData,
+      DirectoryStructure rootDirectoryStructure,
       ModifiableRootModel modifiableRootModel) {
     ImportRoots importRoots =
         ImportRoots.builder(workspaceRoot, Blaze.getBuildSystem(project))
@@ -60,9 +61,9 @@
 
     List<ContentEntry> contentEntries = Lists.newArrayList();
     for (WorkspacePath rootDirectory : rootDirectories) {
-      File root = workspaceRoot.fileForPath(rootDirectory);
+      File rootFile = workspaceRoot.fileForPath(rootDirectory);
       ContentEntry contentEntry =
-          modifiableRootModel.addContentEntry(UrlUtil.pathToUrl(root.getPath()));
+          modifiableRootModel.addContentEntry(UrlUtil.pathToUrl(rootFile.getPath()));
       contentEntries.add(contentEntry);
 
       for (WorkspacePath exclude : excludesByRootDirectory.get(rootDirectory)) {
@@ -72,7 +73,7 @@
 
       ImmutableMap<File, SourceFolder> sourceFolders =
           provider.initializeSourceFolders(contentEntry);
-      SourceFolder rootSource = sourceFolders.get(root);
+      SourceFolder rootSource = sourceFolders.get(rootFile);
       walkFileSystem(
           workspaceRoot,
           testConfig,
@@ -81,7 +82,8 @@
           provider,
           sourceFolders,
           rootSource,
-          root);
+          rootDirectory,
+          rootDirectoryStructure.directories.get(rootDirectory));
     }
   }
 
@@ -93,20 +95,12 @@
       SourceFolderProvider provider,
       ImmutableMap<File, SourceFolder> sourceFolders,
       SourceFolder parent,
-      File file) {
-    if (!FileAttributeProvider.getInstance().isDirectory(file)) {
-      return;
-    }
-    WorkspacePath workspacePath;
-    try {
-      workspacePath = workspaceRoot.workspacePathFor(file);
-    } catch (IllegalArgumentException e) {
-      // stop at directories with unhandled characters.
-      return;
-    }
+      WorkspacePath workspacePath,
+      DirectoryStructure directoryStructure) {
     if (excludedDirectories.contains(workspacePath)) {
       return;
     }
+    File file = workspaceRoot.fileForPath(workspacePath);
     boolean isTest = testConfig.isTestSource(workspacePath.relativePath());
     SourceFolder current = sourceFolders.get(new File(file.getPath()));
     SourceFolder currentOrParent = current != null ? current : parent;
@@ -117,11 +111,8 @@
         contentEntry.removeSourceFolder(current);
       }
     }
-    File[] children = FileAttributeProvider.getInstance().listFiles(file);
-    if (children == null) {
-      return;
-    }
-    for (File child : children) {
+    for (Map.Entry<WorkspacePath, DirectoryStructure> child :
+        directoryStructure.directories.entrySet()) {
       walkFileSystem(
           workspaceRoot,
           testConfig,
@@ -130,7 +121,8 @@
           provider,
           sourceFolders,
           currentOrParent,
-          child);
+          child.getKey(),
+          child.getValue());
     }
   }
 
diff --git a/base/src/com/google/idea/blaze/base/sync/projectstructure/DirectoryStructure.java b/base/src/com/google/idea/blaze/base/sync/projectstructure/DirectoryStructure.java
new file mode 100644
index 0000000..827e13e
--- /dev/null
+++ b/base/src/com/google/idea/blaze/base/sync/projectstructure/DirectoryStructure.java
@@ -0,0 +1,89 @@
+/*
+ * Copyright 2016 The Bazel Authors. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.idea.blaze.base.sync.projectstructure;
+
+import com.google.common.collect.ImmutableMap;
+import com.google.common.util.concurrent.ListenableFuture;
+import com.google.idea.blaze.base.io.FileAttributeProvider;
+import com.google.idea.blaze.base.model.primitives.WorkspacePath;
+import com.google.idea.blaze.base.model.primitives.WorkspaceRoot;
+import com.google.idea.blaze.base.prefetch.FetchExecutor;
+import com.google.idea.blaze.base.projectview.ProjectViewSet;
+import com.google.idea.blaze.base.settings.Blaze;
+import com.google.idea.blaze.base.sync.projectview.ImportRoots;
+import com.intellij.openapi.project.Project;
+import java.io.File;
+import java.util.Collection;
+
+/**
+ * Directory structure representation used by {@link ContentEntryEditor}.
+ *
+ * <p>The purpose of this class is to pull out all file system operations out of the project
+ * structure commit step, as this step locks the UI.
+ */
+public class DirectoryStructure {
+
+  final ImmutableMap<WorkspacePath, DirectoryStructure> directories;
+
+  private DirectoryStructure(ImmutableMap<WorkspacePath, DirectoryStructure> directories) {
+    this.directories = directories;
+  }
+
+  public static ListenableFuture<DirectoryStructure> getRootDirectoryStructure(
+      Project project, WorkspaceRoot workspaceRoot, ProjectViewSet projectViewSet) {
+    return FetchExecutor.EXECUTOR.submit(
+        () -> computeRootDirectoryStructure(project, workspaceRoot, projectViewSet));
+  }
+
+  private static DirectoryStructure computeRootDirectoryStructure(
+      Project project, WorkspaceRoot workspaceRoot, ProjectViewSet projectViewSet) {
+    ImportRoots importRoots =
+        ImportRoots.builder(workspaceRoot, Blaze.getBuildSystem(project))
+            .add(projectViewSet)
+            .build();
+    Collection<WorkspacePath> rootDirectories = importRoots.rootDirectories();
+    ImmutableMap.Builder<WorkspacePath, DirectoryStructure> result = ImmutableMap.builder();
+    for (WorkspacePath rootDirectory : rootDirectories) {
+      walkDirectoryStructure(workspaceRoot, result, rootDirectory);
+    }
+    return new DirectoryStructure(result.build());
+  }
+
+  private static void walkDirectoryStructure(
+      WorkspaceRoot workspaceRoot,
+      ImmutableMap.Builder<WorkspacePath, DirectoryStructure> parent,
+      WorkspacePath workspacePath) {
+    File file = workspaceRoot.fileForPath(workspacePath);
+    if (!FileAttributeProvider.getInstance().isDirectory(file)) {
+      return;
+    }
+    ImmutableMap.Builder<WorkspacePath, DirectoryStructure> result = ImmutableMap.builder();
+    File[] children = FileAttributeProvider.getInstance().listFiles(file);
+    if (children != null) {
+      for (File child : children) {
+        WorkspacePath childWorkspacePath;
+        try {
+          childWorkspacePath = workspaceRoot.workspacePathFor(child);
+        } catch (IllegalArgumentException e) {
+          // stop at directories with unhandled characters.
+          continue;
+        }
+        walkDirectoryStructure(workspaceRoot, result, childWorkspacePath);
+      }
+    }
+    parent.put(workspacePath, new DirectoryStructure(result.build()));
+  }
+}
diff --git a/base/src/com/google/idea/blaze/base/sync/projectstructure/ModuleFinder.java b/base/src/com/google/idea/blaze/base/sync/projectstructure/ModuleFinder.java
new file mode 100644
index 0000000..2973200
--- /dev/null
+++ b/base/src/com/google/idea/blaze/base/sync/projectstructure/ModuleFinder.java
@@ -0,0 +1,32 @@
+/*
+ * Copyright 2017 The Bazel Authors. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.idea.blaze.base.sync.projectstructure;
+
+import com.intellij.openapi.components.ServiceManager;
+import com.intellij.openapi.module.Module;
+import com.intellij.openapi.project.Project;
+import javax.annotation.Nullable;
+
+/** Service for looking up modules (might not be committed during tests). */
+public interface ModuleFinder {
+
+  static ModuleFinder getInstance(Project project) {
+    return ServiceManager.getService(project, ModuleFinder.class);
+  }
+
+  @Nullable
+  Module findModuleByName(String name);
+}
diff --git a/base/src/com/google/idea/blaze/base/sync/projectstructure/ModuleFinderImpl.java b/base/src/com/google/idea/blaze/base/sync/projectstructure/ModuleFinderImpl.java
new file mode 100644
index 0000000..36413f1
--- /dev/null
+++ b/base/src/com/google/idea/blaze/base/sync/projectstructure/ModuleFinderImpl.java
@@ -0,0 +1,37 @@
+/*
+ * Copyright 2017 The Bazel Authors. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.idea.blaze.base.sync.projectstructure;
+
+import com.intellij.openapi.module.Module;
+import com.intellij.openapi.module.ModuleManager;
+import com.intellij.openapi.project.Project;
+import javax.annotation.Nullable;
+
+/** Default implementation of {@link ModuleFinder} */
+public class ModuleFinderImpl implements ModuleFinder {
+
+  private final Project project;
+
+  ModuleFinderImpl(Project project) {
+    this.project = project;
+  }
+
+  @Nullable
+  @Override
+  public Module findModuleByName(String name) {
+    return ModuleManager.getInstance(project).findModuleByName(name);
+  }
+}
diff --git a/base/src/com/google/idea/blaze/base/sync/projectview/ImportRoots.java b/base/src/com/google/idea/blaze/base/sync/projectview/ImportRoots.java
index c484df1..38568dc 100644
--- a/base/src/com/google/idea/blaze/base/sync/projectview/ImportRoots.java
+++ b/base/src/com/google/idea/blaze/base/sync/projectview/ImportRoots.java
@@ -23,16 +23,34 @@
 import com.google.idea.blaze.base.model.primitives.Label;
 import com.google.idea.blaze.base.model.primitives.WorkspacePath;
 import com.google.idea.blaze.base.model.primitives.WorkspaceRoot;
+import com.google.idea.blaze.base.projectview.ProjectViewManager;
 import com.google.idea.blaze.base.projectview.ProjectViewSet;
 import com.google.idea.blaze.base.projectview.section.sections.DirectoryEntry;
 import com.google.idea.blaze.base.projectview.section.sections.DirectorySection;
+import com.google.idea.blaze.base.settings.Blaze;
 import com.google.idea.blaze.base.settings.Blaze.BuildSystem;
 import com.google.idea.blaze.base.util.WorkspacePathUtil;
+import com.intellij.openapi.project.Project;
+import java.nio.file.FileSystems;
+import java.nio.file.Path;
 import java.util.Collection;
 import java.util.Set;
+import javax.annotation.Nullable;
 
 /** The roots to import. Derived from project view. */
 public final class ImportRoots {
+
+  /** Returns the ImportRoots for the project, or null if it's not a blaze project. */
+  @Nullable
+  public static ImportRoots forProjectSafe(Project project) {
+    WorkspaceRoot root = WorkspaceRoot.fromProjectSafe(project);
+    ProjectViewSet projectViewSet = ProjectViewManager.getInstance(project).getProjectViewSet();
+    if (root == null || projectViewSet == null) {
+      return null;
+    }
+    return ImportRoots.builder(root, Blaze.getBuildSystem(project)).add(projectViewSet).build();
+  }
+
   /** Builder for import roots */
   public static class Builder {
     private final ImmutableCollection.Builder<WorkspacePath> rootDirectoriesBuilder =
@@ -123,29 +141,27 @@
   }
 
   private boolean containsLabel(Label label) {
+    return !label.isExternal() && containsWorkspacePath(label.blazePackage());
+  }
+
+  public boolean containsWorkspacePath(WorkspacePath workspacePath) {
     boolean included = false;
     boolean excluded = false;
-    for (WorkspacePath workspacePath : rootDirectories()) {
-      included = included || matchesLabel(workspacePath, label);
+    for (WorkspacePath rootDirectory : rootDirectories()) {
+      included = included || isSubdirectory(rootDirectory, workspacePath);
     }
-    for (WorkspacePath workspacePath : excludeDirectories()) {
-      excluded = excluded || matchesLabel(workspacePath, label);
+    for (WorkspacePath excludeDirectory : excludeDirectories()) {
+      excluded = excluded || isSubdirectory(excludeDirectory, workspacePath);
     }
     return included && !excluded;
   }
 
-  private static boolean matchesLabel(WorkspacePath workspacePath, Label label) {
-    if (workspacePath.isWorkspaceRoot()) {
+  private static boolean isSubdirectory(WorkspacePath ancestor, WorkspacePath descendant) {
+    if (ancestor.isWorkspaceRoot()) {
       return true;
     }
-    String moduleLabelStr = label.toString();
-    int packagePrefixLength = "//".length();
-    int nextCharIndex = workspacePath.relativePath().length() + packagePrefixLength;
-    if (moduleLabelStr.startsWith(workspacePath.relativePath(), packagePrefixLength)
-        && moduleLabelStr.length() >= nextCharIndex) {
-      char c = moduleLabelStr.charAt(nextCharIndex);
-      return c == '/' || c == ':';
-    }
-    return false;
+    Path ancestorPath = FileSystems.getDefault().getPath(ancestor.relativePath());
+    Path descendantPath = FileSystems.getDefault().getPath(descendant.relativePath());
+    return descendantPath.startsWith(ancestorPath);
   }
 }
diff --git a/base/src/com/google/idea/blaze/base/sync/projectview/LanguageSupport.java b/base/src/com/google/idea/blaze/base/sync/projectview/LanguageSupport.java
index da9ae7f..bae9239 100644
--- a/base/src/com/google/idea/blaze/base/sync/projectview/LanguageSupport.java
+++ b/base/src/com/google/idea/blaze/base/sync/projectview/LanguageSupport.java
@@ -15,7 +15,7 @@
  */
 package com.google.idea.blaze.base.sync.projectview;
 
-import com.google.common.collect.Sets;
+import com.google.common.collect.ImmutableSet;
 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;
@@ -25,57 +25,81 @@
 import com.google.idea.blaze.base.scope.output.IssueOutput;
 import com.google.idea.blaze.base.sync.BlazeSyncPlugin;
 import com.intellij.openapi.diagnostic.Logger;
+import java.util.Arrays;
 import java.util.EnumSet;
 import java.util.Set;
+import javax.annotation.Nullable;
 
 /** Reads the user's language preferences from the project view. */
 public class LanguageSupport {
 
   private static final Logger logger = Logger.getInstance(LanguageSupport.class);
 
-  public static WorkspaceLanguageSettings createWorkspaceLanguageSettings(
-      BlazeContext context, ProjectViewSet projectViewSet) {
-    WorkspaceType workspaceType = projectViewSet.getScalarValue(WorkspaceTypeSection.KEY);
-    Set<WorkspaceType> supportedTypes = supportedWorkspaceTypes();
-    if (workspaceType != null && !supportedTypes.contains(workspaceType)) {
-      IssueOutput.error(
-              String.format(
-                  "Workspace type '%s' is not supported by this plugin", workspaceType.getName()))
-          .submit(context);
-      return null;
-    }
-    if (workspaceType == null) {
-      // if no workspace type is specified, prioritize by enum ordinal.
-      for (BlazeSyncPlugin syncPlugin : BlazeSyncPlugin.EP_NAME.getExtensions()) {
-        WorkspaceType recommendedType = syncPlugin.getDefaultWorkspaceType();
-        if (recommendedType != null
-            && (workspaceType == null || workspaceType.ordinal() > recommendedType.ordinal())) {
-          workspaceType = recommendedType;
-        }
+  @Nullable
+  public static WorkspaceType getDefaultWorkspaceType() {
+    WorkspaceType workspaceType = null;
+    // prioritize by enum ordinal.
+    for (BlazeSyncPlugin syncPlugin : BlazeSyncPlugin.EP_NAME.getExtensions()) {
+      WorkspaceType recommendedType = syncPlugin.getDefaultWorkspaceType();
+      if (recommendedType != null
+          && (workspaceType == null || workspaceType.ordinal() > recommendedType.ordinal())) {
+        workspaceType = recommendedType;
       }
     }
+    return workspaceType;
+  }
+
+  /**
+   * Derives {@link WorkspaceLanguageSettings} from the {@link ProjectViewSet}. Does no validation.
+   */
+  @Nullable
+  public static WorkspaceLanguageSettings createWorkspaceLanguageSettings(
+      ProjectViewSet projectViewSet) {
+    WorkspaceType workspaceType = projectViewSet.getScalarValue(WorkspaceTypeSection.KEY);
+    if (workspaceType == null) {
+      workspaceType = getDefaultWorkspaceType();
+    }
 
     if (workspaceType == null) {
       logger.error("Could not find workspace type."); // Should never happen
       return null;
     }
 
-    Set<LanguageClass> activeLanguages = Sets.newHashSet(workspaceType.getLanguages());
-    activeLanguages.addAll(projectViewSet.listItems(AdditionalLanguagesSection.KEY));
+    ImmutableSet.Builder<LanguageClass> activeLanguages =
+        ImmutableSet.<LanguageClass>builder()
+            .addAll(workspaceType.getLanguages())
+            .addAll(projectViewSet.listItems(AdditionalLanguagesSection.KEY))
+            .add(LanguageClass.GENERIC);
+    Arrays.stream(BlazeSyncPlugin.EP_NAME.getExtensions())
+        .forEach(plugin -> activeLanguages.addAll(plugin.getAlwaysActiveLanguages()));
+    return new WorkspaceLanguageSettings(workspaceType, activeLanguages.build());
+  }
 
+  public static boolean validateLanguageSettings(
+      BlazeContext context, WorkspaceLanguageSettings languageSettings) {
+    Set<WorkspaceType> supportedTypes = supportedWorkspaceTypes();
+    WorkspaceType workspaceType = languageSettings.getWorkspaceType();
+    if (!supportedTypes.contains(languageSettings.getWorkspaceType())) {
+      IssueOutput.error(
+              String.format(
+                  "Workspace type '%s' is not supported by this plugin",
+                  languageSettings.getWorkspaceType().getName()))
+          .submit(context);
+      return false;
+    }
     Set<LanguageClass> supportedLanguages = supportedLanguagesForWorkspaceType(workspaceType);
     Set<LanguageClass> availableLanguages = EnumSet.noneOf(LanguageClass.class);
     for (WorkspaceType type : supportedTypes) {
       availableLanguages.addAll(supportedLanguagesForWorkspaceType(type));
     }
 
-    for (LanguageClass languageClass : activeLanguages) {
+    for (LanguageClass languageClass : languageSettings.activeLanguages) {
       if (!availableLanguages.contains(languageClass)) {
         IssueOutput.error(
                 String.format(
                     "Language '%s' is not supported by this plugin", languageClass.getName()))
             .submit(context);
-        return null;
+        return false;
       }
       if (!supportedLanguages.contains(languageClass)) {
         IssueOutput.error(
@@ -83,12 +107,10 @@
                     "Language '%s' is not supported for this plugin with workspace type: '%s'",
                     languageClass.getName(), workspaceType.getName()))
             .submit(context);
-        return null;
+        return false;
       }
     }
-
-    activeLanguages.add(LanguageClass.GENERIC);
-    return new WorkspaceLanguageSettings(workspaceType, activeLanguages);
+    return true;
   }
 
   /** The {@link WorkspaceType}s supported by this plugin */
@@ -105,7 +127,21 @@
     Set<LanguageClass> supportedLanguages = EnumSet.noneOf(LanguageClass.class);
     for (BlazeSyncPlugin syncPlugin : BlazeSyncPlugin.EP_NAME.getExtensions()) {
       supportedLanguages.addAll(syncPlugin.getSupportedLanguagesInWorkspace(type));
+      supportedLanguages.addAll(syncPlugin.getAlwaysActiveLanguages());
     }
+    supportedLanguages.add(LanguageClass.GENERIC);
     return supportedLanguages;
   }
+
+  /** @return The valid 'additional_language' options for this workspace type */
+  public static Set<LanguageClass> availableAdditionalLanguages(WorkspaceType workspaceType) {
+    Set<LanguageClass> langs = LanguageSupport.supportedLanguagesForWorkspaceType(workspaceType);
+    langs.removeAll(workspaceType.getLanguages());
+    langs.remove(LanguageClass.GENERIC);
+
+    Arrays.stream(BlazeSyncPlugin.EP_NAME.getExtensions())
+        .forEach(plugin -> langs.removeAll(plugin.getAlwaysActiveLanguages()));
+
+    return langs;
+  }
 }
diff --git a/base/src/com/google/idea/blaze/base/sync/projectview/RelatedWorkspacePathFinder.java b/base/src/com/google/idea/blaze/base/sync/projectview/RelatedWorkspacePathFinder.java
new file mode 100644
index 0000000..9006605
--- /dev/null
+++ b/base/src/com/google/idea/blaze/base/sync/projectview/RelatedWorkspacePathFinder.java
@@ -0,0 +1,70 @@
+/*
+ * Copyright 2017 The Bazel Authors. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.idea.blaze.base.sync.projectview;
+
+import com.google.common.collect.ImmutableSet;
+import com.google.idea.blaze.base.io.FileAttributeProvider;
+import com.google.idea.blaze.base.model.primitives.WorkspacePath;
+import com.google.idea.blaze.base.sync.workspace.WorkspacePathResolver;
+import com.intellij.openapi.components.ServiceManager;
+import java.io.File;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+
+/** Utility class to find WorkspacePaths that are related */
+public final class RelatedWorkspacePathFinder {
+
+  private final FileAttributeProvider fileAttributeProvider;
+
+  RelatedWorkspacePathFinder(FileAttributeProvider fileAttributeProvider) {
+    this.fileAttributeProvider = fileAttributeProvider;
+  }
+
+  public static RelatedWorkspacePathFinder getInstance() {
+    return ServiceManager.getService(RelatedWorkspacePathFinder.class);
+  }
+
+  public ImmutableSet<WorkspacePath> findRelatedWorkspaceDirectories(
+      WorkspacePathResolver pathResolver, WorkspacePath workspacePath) {
+
+    Path path = Paths.get(workspacePath.relativePath());
+    Path testsPath = Paths.get("");
+
+    boolean foundTests = false;
+    for (Path element : path) {
+      if (!foundTests && element.toString().equals("java")) {
+        Path potentialTestsPath = testsPath.resolve("javatests");
+        if (exists(pathResolver.resolveToFile(potentialTestsPath.toString()))) {
+          testsPath = potentialTestsPath;
+          foundTests = true;
+          continue;
+        }
+      }
+
+      testsPath = testsPath.resolve(element);
+    }
+
+    if (!foundTests || !exists(pathResolver.resolveToFile(testsPath.toString()))) {
+      return ImmutableSet.of();
+    }
+
+    return ImmutableSet.of(new WorkspacePath(testsPath.toString()));
+  }
+
+  private boolean exists(File file) {
+    return fileAttributeProvider.exists(file);
+  }
+}
diff --git a/base/src/com/google/idea/blaze/base/sync/projectview/WorkspaceLanguageSettings.java b/base/src/com/google/idea/blaze/base/sync/projectview/WorkspaceLanguageSettings.java
index 4ebde4d..5fdd67f 100644
--- a/base/src/com/google/idea/blaze/base/sync/projectview/WorkspaceLanguageSettings.java
+++ b/base/src/com/google/idea/blaze/base/sync/projectview/WorkspaceLanguageSettings.java
@@ -16,10 +16,12 @@
 package com.google.idea.blaze.base.sync.projectview;
 
 import com.google.common.base.Objects;
+import com.google.common.collect.ImmutableSet;
+import com.google.idea.blaze.base.model.primitives.Kind;
 import com.google.idea.blaze.base.model.primitives.LanguageClass;
 import com.google.idea.blaze.base.model.primitives.WorkspaceType;
 import java.io.Serializable;
-import java.util.Set;
+import java.util.EnumSet;
 import javax.annotation.concurrent.Immutable;
 
 /** Contains the user's language preferences from the project view. */
@@ -28,10 +30,10 @@
   private static final long serialVersionUID = 1L;
 
   private final WorkspaceType workspaceType;
-  private final Set<LanguageClass> activeLanguages;
+  final ImmutableSet<LanguageClass> activeLanguages;
 
   public WorkspaceLanguageSettings(
-      WorkspaceType workspaceType, Set<LanguageClass> activeLanguages) {
+      WorkspaceType workspaceType, ImmutableSet<LanguageClass> activeLanguages) {
     this.workspaceType = workspaceType;
     this.activeLanguages = activeLanguages;
   }
@@ -57,6 +59,12 @@
     return activeLanguages.contains(languageClass);
   }
 
+  public EnumSet<Kind> getAvailableTargetKinds() {
+    EnumSet<Kind> kinds = EnumSet.allOf(Kind.class);
+    kinds.removeIf(kind -> !activeLanguages.contains(kind.getLanguageClass()));
+    return kinds;
+  }
+
   @Override
   public boolean equals(Object o) {
     if (this == o) {
diff --git a/base/src/com/google/idea/blaze/base/sync/sharding/BlazeBuildTargetSharder.java b/base/src/com/google/idea/blaze/base/sync/sharding/BlazeBuildTargetSharder.java
new file mode 100644
index 0000000..8f2279c
--- /dev/null
+++ b/base/src/com/google/idea/blaze/base/sync/sharding/BlazeBuildTargetSharder.java
@@ -0,0 +1,176 @@
+/*
+ * Copyright 2017 The Bazel Authors. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.idea.blaze.base.sync.sharding;
+
+import com.google.common.collect.ImmutableList;
+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.ShardBlazeBuildsSection;
+import com.google.idea.blaze.base.scope.BlazeContext;
+import com.google.idea.blaze.base.sync.aspects.BuildResult;
+import com.google.idea.blaze.base.sync.sharding.WildcardTargetExpander.ExpandedTargetsResult;
+import com.google.idea.blaze.base.sync.workspace.WorkspacePathResolver;
+import com.google.idea.common.experiments.BoolExperiment;
+import com.intellij.openapi.project.Project;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.stream.Collectors;
+
+/** Utility methods for sharding blaze build invocations. */
+public class BlazeBuildTargetSharder {
+
+  private static final BoolExperiment allowSharding =
+      new BoolExperiment("blaze.build.sharding.allowed", true);
+
+  // number of packages per blaze query shard
+  static final int PACKAGE_SHARD_SIZE = 500;
+
+  // number of individual targets per blaze build shard
+  private static final int TARGET_SHARD_SIZE = 1000;
+
+  /** Result of expanding then sharding wildcard target patterns */
+  public static class ShardedTargetsResult {
+    public final ShardedTargetList shardedTargets;
+    public final BuildResult buildResult;
+
+    private ShardedTargetsResult(ShardedTargetList shardedTargets, BuildResult buildResult) {
+      this.shardedTargets = shardedTargets;
+      this.buildResult = buildResult;
+    }
+  }
+
+  /** Returns true if sharding can be enabled for this project, and is not already enabled */
+  static boolean canEnableSharding(Project project) {
+    if (!allowSharding.getValue()) {
+      return false;
+    }
+    ProjectViewSet projectViewSet = ProjectViewManager.getInstance(project).getProjectViewSet();
+    return projectViewSet != null && !shardingEnabled(projectViewSet);
+  }
+
+  private static boolean shardingEnabled(ProjectViewSet projectViewSet) {
+    if (!allowSharding.getValue()) {
+      return false;
+    }
+    return projectViewSet.getScalarValue(ShardBlazeBuildsSection.KEY, false);
+  }
+
+  /** Expand wildcard target patterns and partition the resulting target list. */
+  public static ShardedTargetsResult expandAndShardTargets(
+      Project project,
+      BlazeContext context,
+      WorkspaceRoot workspaceRoot,
+      ProjectViewSet projectViewSet,
+      WorkspacePathResolver pathResolver,
+      List<TargetExpression> targets) {
+    if (!shardingEnabled(projectViewSet)) {
+      return new ShardedTargetsResult(
+          new ShardedTargetList(ImmutableList.of(targets)), BuildResult.SUCCESS);
+    }
+
+    List<WildcardTargetPattern> wildcardIncludes = getWildcardPatterns(targets);
+    if (wildcardIncludes.isEmpty()) {
+      return new ShardedTargetsResult(
+          new ShardedTargetList(ImmutableList.of(targets)), BuildResult.SUCCESS);
+    }
+    ExpandedTargetsResult expandedTargets =
+        expandWildcardTargets(
+            project, context, workspaceRoot, projectViewSet, pathResolver, targets);
+    if (expandedTargets.buildResult.status == BuildResult.Status.FATAL_ERROR) {
+      return new ShardedTargetsResult(
+          new ShardedTargetList(ImmutableList.of()), expandedTargets.buildResult);
+    }
+    return new ShardedTargetsResult(
+        shardTargets(expandedTargets.singleTargets, TARGET_SHARD_SIZE),
+        expandedTargets.buildResult);
+  }
+
+  /** Expand wildcard target patterns into individual blaze targets. */
+  private static ExpandedTargetsResult expandWildcardTargets(
+      Project project,
+      BlazeContext context,
+      WorkspaceRoot workspaceRoot,
+      ProjectViewSet projectViewSet,
+      WorkspacePathResolver pathResolver,
+      List<TargetExpression> targets) {
+    if (!shardingEnabled(projectViewSet)) {
+      return new ExpandedTargetsResult(targets, BuildResult.SUCCESS);
+    }
+    List<WildcardTargetPattern> includes = getWildcardPatterns(targets);
+    if (includes.isEmpty()) {
+      return new ExpandedTargetsResult(targets, BuildResult.SUCCESS);
+    }
+    Map<TargetExpression, List<TargetExpression>> expandedTargets =
+        WildcardTargetExpander.expandToNonRecursiveWildcardTargets(
+            project, context, pathResolver, includes);
+    if (expandedTargets == null) {
+      return new ExpandedTargetsResult(ImmutableList.of(), BuildResult.FATAL_ERROR);
+    }
+
+    // replace original recursive targets with expanded list, retaining relative ordering
+    List<TargetExpression> fullList = new ArrayList<>();
+    for (TargetExpression target : targets) {
+      List<TargetExpression> expanded = expandedTargets.get(target);
+      if (expanded == null) {
+        fullList.add(target);
+      } else {
+        fullList.addAll(expanded);
+      }
+    }
+    return WildcardTargetExpander.expandToSingleTargets(
+        project, context, workspaceRoot, projectViewSet, fullList);
+  }
+
+  /**
+   * Partition targets list. Because order is important with respect to excluded targets, each shard
+   * has all subsequent excluded targets appended to it.
+   */
+  static ShardedTargetList shardTargets(List<TargetExpression> targets, int shardSize) {
+    if (targets.size() <= shardSize) {
+      return new ShardedTargetList(ImmutableList.of(targets));
+    }
+    List<List<TargetExpression>> output = new ArrayList<>();
+    for (int index = 0; index < targets.size(); index += shardSize) {
+      int endIndex = Math.min(targets.size(), index + shardSize);
+      List<TargetExpression> shard = new ArrayList<>(targets.subList(index, endIndex));
+      List<TargetExpression> remainingExcludes =
+          targets
+              .subList(endIndex, targets.size())
+              .stream()
+              .filter(TargetExpression::isExcluded)
+              .collect(Collectors.toList());
+      shard.addAll(remainingExcludes);
+      output.add(shard);
+    }
+    return new ShardedTargetList(output);
+  }
+
+  /** Returns the wildcard target patterns, ignoring exclude patterns (those starting with '-') */
+  private static List<WildcardTargetPattern> getWildcardPatterns(List<TargetExpression> targets) {
+    return targets
+        .stream()
+        .filter(t -> !t.isExcluded())
+        .map(WildcardTargetPattern::fromExpression)
+        .filter(Objects::nonNull)
+        .collect(Collectors.toList());
+  }
+
+  private BlazeBuildTargetSharder() {}
+}
diff --git a/base/src/com/google/idea/blaze/base/sync/sharding/PackageLister.java b/base/src/com/google/idea/blaze/base/sync/sharding/PackageLister.java
new file mode 100644
index 0000000..d87d205
--- /dev/null
+++ b/base/src/com/google/idea/blaze/base/sync/sharding/PackageLister.java
@@ -0,0 +1,140 @@
+/*
+ * Copyright 2017 The Bazel Authors. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.idea.blaze.base.sync.sharding;
+
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.Lists;
+import com.google.common.collect.Maps;
+import com.google.common.util.concurrent.Futures;
+import com.google.common.util.concurrent.ListenableFuture;
+import com.google.idea.blaze.base.async.FutureUtil;
+import com.google.idea.blaze.base.async.FutureUtil.FutureResult;
+import com.google.idea.blaze.base.bazel.BuildSystemProvider;
+import com.google.idea.blaze.base.io.FileAttributeProvider;
+import com.google.idea.blaze.base.model.primitives.TargetExpression;
+import com.google.idea.blaze.base.model.primitives.WorkspacePath;
+import com.google.idea.blaze.base.prefetch.FetchExecutor;
+import com.google.idea.blaze.base.scope.BlazeContext;
+import com.google.idea.blaze.base.sync.workspace.WorkspacePathResolver;
+import com.google.idea.blaze.base.util.WorkspacePathUtil;
+import java.io.File;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.Set;
+import java.util.function.Predicate;
+import java.util.stream.Collectors;
+import javax.annotation.Nullable;
+
+/**
+ * Traverses blaze packages specified by wildcard target patterns, expanding to a set of
+ * single-package target patterns.
+ */
+class PackageLister {
+
+  private PackageLister() {}
+
+  /** The set of blaze packages to prefetch prior to traversing the directory tree. */
+  static Set<File> getDirectoriesToPrefetch(
+      WorkspacePathResolver pathResolver,
+      Collection<WildcardTargetPattern> includes,
+      Predicate<WorkspacePath> excluded) {
+    Set<WorkspacePath> prefetchPaths = new HashSet<>();
+    for (WildcardTargetPattern pattern : includes) {
+      WorkspacePath workspacePath = pattern.getBasePackage();
+      if (excluded.test(workspacePath)) {
+        continue;
+      }
+      prefetchPaths.add(workspacePath);
+    }
+    return WorkspacePathUtil.calculateMinimalWorkspacePaths(prefetchPaths)
+        .stream()
+        .map(pathResolver::resolveToFile)
+        .collect(Collectors.toSet());
+  }
+
+  /**
+   * Expands all-in-package-recursive wildcard targets into all-in-single-package targets by
+   * traversing the file system, looking for child blaze packages.
+   *
+   * <p>Returns null if directory traversal failed or was cancelled.
+   */
+  @Nullable
+  static Map<TargetExpression, List<TargetExpression>> expandPackageTargets(
+      BuildSystemProvider provider,
+      BlazeContext context,
+      WorkspacePathResolver pathResolver,
+      Collection<WildcardTargetPattern> wildcardPatterns) {
+    List<ListenableFuture<Entry<TargetExpression, List<TargetExpression>>>> futures =
+        Lists.newArrayList();
+    for (WildcardTargetPattern pattern : wildcardPatterns) {
+      if (!pattern.isRecursive() || pattern.toString().startsWith("-")) {
+        continue;
+      }
+      File dir = pathResolver.resolveToFile(pattern.getBasePackage());
+      if (!FileAttributeProvider.getInstance().isDirectory(dir)) {
+        continue;
+      }
+      futures.add(
+          FetchExecutor.EXECUTOR.submit(
+              () -> {
+                List<TargetExpression> expandedTargets = new ArrayList<>();
+                traversePackageRecursively(provider, pathResolver, dir, expandedTargets);
+                return Maps.immutableEntry(pattern.originalPattern, expandedTargets);
+              }));
+    }
+    if (futures.isEmpty()) {
+      return ImmutableMap.of();
+    }
+    FutureResult<List<Entry<TargetExpression, List<TargetExpression>>>> result =
+        FutureUtil.waitForFuture(context, Futures.allAsList(futures))
+            .withProgressMessage("Expanding wildcard target patterns...")
+            .timed("ExpandWildcardTargets")
+            .onError("Expanding wildcard target patterns failed")
+            .run();
+    if (!result.success()) {
+      return null;
+    }
+    return result
+        .result()
+        .stream()
+        .collect(Collectors.toMap(Entry::getKey, Entry::getValue, (x, y) -> x));
+  }
+
+  private static void traversePackageRecursively(
+      BuildSystemProvider provider,
+      WorkspacePathResolver pathResolver,
+      File dir,
+      List<TargetExpression> output) {
+    WorkspacePath path = pathResolver.getWorkspacePath(dir);
+    if (path == null) {
+      return;
+    }
+    if (provider.findBuildFileInDirectory(dir) != null) {
+      output.add(TargetExpression.allFromPackageNonRecursive(path));
+    }
+    File[] children = FileAttributeProvider.getInstance().listFiles(dir);
+    if (children == null) {
+      return;
+    }
+    for (File child : children) {
+      traversePackageRecursively(provider, pathResolver, child, output);
+    }
+  }
+}
diff --git a/base/src/com/google/idea/blaze/base/sync/sharding/QueryResultLineProcessor.java b/base/src/com/google/idea/blaze/base/sync/sharding/QueryResultLineProcessor.java
new file mode 100644
index 0000000..8d7a46a
--- /dev/null
+++ b/base/src/com/google/idea/blaze/base/sync/sharding/QueryResultLineProcessor.java
@@ -0,0 +1,67 @@
+/*
+ * Copyright 2017 The Bazel Authors. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.idea.blaze.base.sync.sharding;
+
+import com.google.common.collect.ImmutableList;
+import com.google.idea.blaze.base.async.process.LineProcessingOutputStream;
+import com.google.idea.blaze.base.model.primitives.TargetExpression;
+import java.util.function.Predicate;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/** Collects the blaze targets output by 'blaze query --output label_kind "targets"' */
+class QueryResultLineProcessor implements LineProcessingOutputStream.LineProcessor {
+
+  static class RuleTypeAndLabel {
+    final String ruleType;
+    final String label;
+
+    private RuleTypeAndLabel(String ruleType, String label) {
+      this.ruleType = ruleType;
+      this.label = label;
+    }
+  }
+
+  private static final Pattern RULE_PATTERN = Pattern.compile("^([^\\s]*) rule ([^\\s]*)$");
+
+  private ImmutableList.Builder<TargetExpression> outputList;
+  private final Predicate<RuleTypeAndLabel> targetFilter;
+
+  /**
+   * @param outputList Parsed target expressions are added to this list
+   * @param targetFilter Ignore targets failing this predicate.
+   */
+  QueryResultLineProcessor(
+      ImmutableList.Builder<TargetExpression> outputList,
+      Predicate<RuleTypeAndLabel> targetFilter) {
+    this.outputList = outputList;
+    this.targetFilter = targetFilter;
+  }
+
+  @Override
+  public boolean processLine(String line) {
+    Matcher match = RULE_PATTERN.matcher(line);
+    if (!match.find()) {
+      return true;
+    }
+    String ruleType = match.group(1);
+    String label = match.group(2);
+    if (targetFilter.test(new RuleTypeAndLabel(ruleType, label))) {
+      outputList.add(TargetExpression.fromString(label));
+    }
+    return true;
+  }
+}
diff --git a/base/src/com/google/idea/blaze/base/sync/sharding/ShardedTargetList.java b/base/src/com/google/idea/blaze/base/sync/sharding/ShardedTargetList.java
new file mode 100644
index 0000000..139c3b8
--- /dev/null
+++ b/base/src/com/google/idea/blaze/base/sync/sharding/ShardedTargetList.java
@@ -0,0 +1,83 @@
+/*
+ * Copyright 2017 The Bazel Authors. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.idea.blaze.base.sync.sharding;
+
+import com.google.idea.blaze.base.model.primitives.TargetExpression;
+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.Blaze;
+import com.google.idea.blaze.base.sync.aspects.BuildResult;
+import com.intellij.openapi.project.Project;
+import java.util.List;
+import java.util.function.Function;
+
+/** Partitioned list of blaze targets. */
+public class ShardedTargetList {
+
+  public final List<List<TargetExpression>> shardedTargets;
+
+  public ShardedTargetList(List<List<TargetExpression>> shardedTargets) {
+    this.shardedTargets = shardedTargets;
+  }
+
+  public boolean isEmpty() {
+    return shardedTargets.stream().flatMap(List::stream).findFirst().orElse(null) == null;
+  }
+
+  /**
+   * Runs the provided blaze invocation on each target list shard, returning the combined {@link
+   * BuildResult}. Attempts to work around out of memory errors caused by lack of blaze garbage
+   * collection where possible.
+   */
+  public BuildResult runShardedCommand(
+      Project project,
+      BlazeContext context,
+      Function<Integer, String> progressMessage,
+      Function<List<TargetExpression>, BuildResult> invocation) {
+    if (isEmpty()) {
+      return BuildResult.SUCCESS;
+    }
+    if (shardedTargets.size() == 1) {
+      return invocation.apply(shardedTargets.get(0));
+    }
+    int progress = 0;
+    BuildResult output = null;
+    for (int i = 0; i < shardedTargets.size(); i++, progress++) {
+      context.output(new StatusOutput(progressMessage.apply(i + 1)));
+      BuildResult result = invocation.apply(shardedTargets.get(i));
+      if (result.outOfMemory() && progress > 0) {
+        // re-try now that blaze server has restarted
+        progress = 0;
+        IssueOutput.warn(retryOnOomMessage(project, i)).submit(context);
+        result = invocation.apply(shardedTargets.get(i));
+      }
+      output = output == null ? result : BuildResult.combine(output, result);
+      if (output.status == BuildResult.Status.FATAL_ERROR) {
+        return output;
+      }
+    }
+    return output;
+  }
+
+  private String retryOnOomMessage(Project project, int shardIndex) {
+    String buildSystem = Blaze.buildSystemName(project);
+    return String.format(
+        "%s server ran out of memory on shard %s of %s. This is generally caused by %s garbage "
+            + "collection bugs. Attempting to workaround by resuming with a clean %s server.",
+        buildSystem, shardIndex + 1, shardedTargets.size(), buildSystem, buildSystem);
+  }
+}
diff --git a/base/src/com/google/idea/blaze/base/sync/sharding/SuggestEnablingShardingNotification.java b/base/src/com/google/idea/blaze/base/sync/sharding/SuggestEnablingShardingNotification.java
new file mode 100644
index 0000000..71f69c0
--- /dev/null
+++ b/base/src/com/google/idea/blaze/base/sync/sharding/SuggestEnablingShardingNotification.java
@@ -0,0 +1,103 @@
+/*
+ * Copyright 2017 The Bazel Authors. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.idea.blaze.base.sync.sharding;
+
+import com.google.idea.blaze.base.projectview.ProjectViewEdit;
+import com.google.idea.blaze.base.projectview.section.ScalarSection;
+import com.google.idea.blaze.base.projectview.section.sections.ShardBlazeBuildsSection;
+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.settings.BlazeUserSettings;
+import com.google.idea.blaze.base.sync.BlazeSyncManager;
+import com.google.idea.blaze.base.sync.BlazeSyncParams;
+import com.intellij.notification.Notification;
+import com.intellij.notification.NotificationListener;
+import com.intellij.notification.NotificationType;
+import com.intellij.openapi.application.ApplicationManager;
+import com.intellij.openapi.project.Project;
+import com.intellij.openapi.ui.Messages;
+import com.intellij.pom.NavigatableAdapter;
+import com.intellij.xml.util.XmlStringUtil;
+import javax.swing.event.HyperlinkEvent;
+
+/** If blaze runs out of memory during sync, suggest that the user enables build sharding. */
+public class SuggestEnablingShardingNotification {
+
+  public static void suggestSharding(Project project, BlazeContext context) {
+    if (!BlazeBuildTargetSharder.canEnableSharding(project)) {
+      return;
+    }
+    String buildSystem = Blaze.buildSystemName(project);
+    String message =
+        String.format(
+            "The %1$s server ran out of memory during sync. This can occur for large projects. You "
+                + "can workaround this by <a href='fix'>sharding the %1$s build during sync</a>, "
+                + "or alternatively allocate more memory to %1$s",
+            buildSystem);
+    IssueOutput.error(message)
+        .navigatable(
+            new NavigatableAdapter() {
+              @Override
+              public void navigate(boolean requestFocus) {
+                enableShardingAndResync(project);
+              }
+            })
+        .submit(context);
+
+    Notification notification =
+        new Notification(
+            "Out of memory during sync",
+            buildSystem + " ran out of memory during sync",
+            XmlStringUtil.wrapInHtml(message),
+            NotificationType.ERROR,
+            new NotificationListener.Adapter() {
+              @Override
+              protected void hyperlinkActivated(
+                  Notification notification, HyperlinkEvent hyperlinkEvent) {
+                notification.expire();
+                enableShardingAndResync(project);
+              }
+            });
+    notification.setImportant(true);
+    ApplicationManager.getApplication().invokeLater(() -> notification.notify(project));
+  }
+
+  private static void enableShardingAndResync(Project project) {
+    ProjectViewEdit edit =
+        ProjectViewEdit.editLocalProjectView(
+            project,
+            builder -> {
+              ScalarSection<Boolean> existingSection = builder.getLast(ShardBlazeBuildsSection.KEY);
+              builder.replace(
+                  existingSection, ScalarSection.builder(ShardBlazeBuildsSection.KEY).set(true));
+              return true;
+            });
+    if (edit == null) {
+      Messages.showErrorDialog(
+          "Could not modify project view. Check for errors in your project view and try again",
+          "Error");
+      return;
+    }
+    edit.apply();
+    BlazeSyncManager.getInstance(project)
+        .requestProjectSync(
+            new BlazeSyncParams.Builder("Sync", BlazeSyncParams.SyncMode.INCREMENTAL)
+                .addProjectViewTargets(true)
+                .addWorkingSet(BlazeUserSettings.getInstance().getExpandSyncToWorkingSet())
+                .build());
+  }
+}
diff --git a/base/src/com/google/idea/blaze/base/sync/sharding/WildcardTargetExpander.java b/base/src/com/google/idea/blaze/base/sync/sharding/WildcardTargetExpander.java
new file mode 100644
index 0000000..396c1cf
--- /dev/null
+++ b/base/src/com/google/idea/blaze/base/sync/sharding/WildcardTargetExpander.java
@@ -0,0 +1,227 @@
+/*
+ * Copyright 2017 The Bazel Authors. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.idea.blaze.base.sync.sharding;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableSet;
+import com.google.common.util.concurrent.ListenableFuture;
+import com.google.idea.blaze.base.async.FutureUtil;
+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.bazel.BuildSystemProvider;
+import com.google.idea.blaze.base.command.BlazeCommand;
+import com.google.idea.blaze.base.command.BlazeCommandName;
+import com.google.idea.blaze.base.command.BlazeFlags;
+import com.google.idea.blaze.base.model.primitives.Kind;
+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;
+import com.google.idea.blaze.base.prefetch.PrefetchService;
+import com.google.idea.blaze.base.projectview.ProjectViewSet;
+import com.google.idea.blaze.base.scope.BlazeContext;
+import com.google.idea.blaze.base.scope.output.StatusOutput;
+import com.google.idea.blaze.base.settings.Blaze;
+import com.google.idea.blaze.base.sync.aspects.BuildResult;
+import com.google.idea.blaze.base.sync.projectview.LanguageSupport;
+import com.google.idea.blaze.base.sync.sharding.QueryResultLineProcessor.RuleTypeAndLabel;
+import com.google.idea.blaze.base.sync.workspace.WorkspacePathResolver;
+import com.google.idea.common.experiments.BoolExperiment;
+import com.intellij.openapi.project.Project;
+import java.io.File;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.function.Predicate;
+import java.util.stream.Collectors;
+import javax.annotation.Nullable;
+
+/** Expands wildcard target patterns into individual blaze targets. */
+public class WildcardTargetExpander {
+
+  private static final BoolExperiment filterByRuleType =
+      new BoolExperiment("blaze.build.filter.by.rule.type", true);
+
+  static class ExpandedTargetsResult {
+    final List<TargetExpression> singleTargets;
+    final BuildResult buildResult;
+
+    ExpandedTargetsResult(List<TargetExpression> singleTargets, BuildResult buildResult) {
+      this.singleTargets = singleTargets;
+      this.buildResult = buildResult;
+    }
+
+    static ExpandedTargetsResult merge(ExpandedTargetsResult first, ExpandedTargetsResult second) {
+      BuildResult buildResult = BuildResult.combine(first.buildResult, second.buildResult);
+      List<TargetExpression> targets =
+          ImmutableList.<TargetExpression>builder()
+              .addAll(first.singleTargets)
+              .addAll(second.singleTargets)
+              .build();
+      return new ExpandedTargetsResult(targets, buildResult);
+    }
+  }
+
+  /**
+   * Expand recursive wildcard blaze target patterns into single-package wildcard patterns, via a
+   * file system traversal.
+   *
+   * <p>Exclude target patterns (beginning with '-') are not expanded.
+   *
+   * <p>Returns null if operation failed or was cancelled.
+   */
+  @Nullable
+  static Map<TargetExpression, List<TargetExpression>> expandToNonRecursiveWildcardTargets(
+      Project project,
+      BlazeContext context,
+      WorkspacePathResolver pathResolver,
+      List<WildcardTargetPattern> wildcardPatterns) {
+
+    Set<WildcardTargetPattern> excludes =
+        wildcardPatterns
+            .stream()
+            .filter(WildcardTargetPattern::isExcluded)
+            .collect(Collectors.toSet());
+
+    Predicate<WorkspacePath> excludePredicate =
+        workspacePath ->
+            excludes.stream().anyMatch(pattern -> pattern.coversPackage(workspacePath));
+
+    List<WildcardTargetPattern> includes = new ArrayList<>(wildcardPatterns);
+    includes.removeAll(excludes);
+
+    Set<File> toPrefetch =
+        PackageLister.getDirectoriesToPrefetch(pathResolver, includes, excludePredicate);
+
+    ListenableFuture<?> prefetchFuture =
+        PrefetchService.getInstance().prefetchFiles(project, toPrefetch);
+    if (!FutureUtil.waitForFuture(context, prefetchFuture)
+        .withProgressMessage("Prefetching wildcard target pattern directories...")
+        .timed("PrefetchingWildcardTargetDirectories")
+        .onError("Prefetching wildcard target directories failed")
+        .run()
+        .success()) {
+      return null;
+    }
+
+    return PackageLister.expandPackageTargets(
+        Blaze.getBuildSystemProvider(project), context, pathResolver, includes);
+  }
+
+  /** Runs a sharded blaze query to expand wildcard targets to individual blaze targets */
+  static ExpandedTargetsResult expandToSingleTargets(
+      Project project,
+      BlazeContext context,
+      WorkspaceRoot workspaceRoot,
+      ProjectViewSet projectViewSet,
+      List<TargetExpression> allTargets) {
+    ShardedTargetList shards =
+        BlazeBuildTargetSharder.shardTargets(
+            allTargets, BlazeBuildTargetSharder.PACKAGE_SHARD_SIZE);
+    ImmutableSet<String> handledRuleTypes = handledRuleTypes(projectViewSet);
+    ExpandedTargetsResult output = null;
+    for (int i = 0; i < shards.shardedTargets.size(); i++) {
+      List<TargetExpression> shard = shards.shardedTargets.get(i);
+      context.output(
+          new StatusOutput(
+              String.format(
+                  "Expanding wildcard target patterns, shard %s of %s",
+                  i + 1, shards.shardedTargets.size())));
+      ExpandedTargetsResult result =
+          queryIndividualTargets(project, context, workspaceRoot, handledRuleTypes, shard);
+      output = output == null ? result : ExpandedTargetsResult.merge(output, result);
+      if (output.buildResult == BuildResult.FATAL_ERROR) {
+        return output;
+      }
+    }
+    return output;
+  }
+
+  /** Runs a blaze query to expand the input target patterns to individual blaze targets. */
+  private static ExpandedTargetsResult queryIndividualTargets(
+      Project project,
+      BlazeContext context,
+      WorkspaceRoot workspaceRoot,
+      ImmutableSet<String> handledRuleTypes,
+      List<TargetExpression> targetPatterns) {
+    BlazeCommand.Builder builder =
+        BlazeCommand.builder(getBinaryPath(project), BlazeCommandName.QUERY)
+            .addBlazeFlags(BlazeFlags.KEEP_GOING)
+            .addBlazeFlags("--output=label_kind")
+            .addBlazeFlags(queryString(targetPatterns));
+
+    ImmutableList.Builder<TargetExpression> output = ImmutableList.builder();
+
+    // it's fine to include wildcards here; they're guaranteed not to clash with actual labels.
+    Set<String> explicitTargets =
+        targetPatterns.stream().map(TargetExpression::toString).collect(Collectors.toSet());
+    Predicate<RuleTypeAndLabel> filter =
+        !filterByRuleType.getValue()
+            ? t -> true
+            : t -> handledRuleTypes.contains(t.ruleType) || explicitTargets.contains(t.label);
+
+    int retVal =
+        ExternalTask.builder(workspaceRoot)
+            .addBlazeCommand(builder.build())
+            .context(context)
+            .stdout(LineProcessingOutputStream.of(new QueryResultLineProcessor(output, filter)))
+            .stderr(LineProcessingOutputStream.of(new PrintOutputLineProcessor(context)))
+            .build()
+            .run();
+
+    BuildResult buildResult = BuildResult.fromExitCode(retVal);
+    return new ExpandedTargetsResult(output.build(), buildResult);
+  }
+
+  private static ImmutableSet<String> handledRuleTypes(ProjectViewSet projectViewSet) {
+    return ImmutableSet.copyOf(
+        LanguageSupport.createWorkspaceLanguageSettings(projectViewSet)
+            .getAvailableTargetKinds()
+            .stream()
+            .map(Kind::toString)
+            .collect(Collectors.toList()));
+  }
+
+  private static String queryString(List<TargetExpression> targets) {
+    StringBuilder builder = new StringBuilder();
+    for (TargetExpression target : targets) {
+      boolean excluded = target.isExcluded();
+      if (builder.length() == 0) {
+        if (excluded) {
+          continue; // an excluded target at the start of the list has no effect
+        }
+        builder.append(target);
+      } else {
+        if (excluded) {
+          builder.append(" - ");
+          // trim leading '-'
+          String excludedTarget = target.toString();
+          builder.append(excludedTarget, 1, excludedTarget.length());
+        } else {
+          builder.append(" + ");
+          builder.append(target);
+        }
+      }
+    }
+    return builder.toString();
+  }
+
+  private static String getBinaryPath(Project project) {
+    BuildSystemProvider buildSystemProvider = Blaze.getBuildSystemProvider(project);
+    return buildSystemProvider.getSyncBinaryPath();
+  }
+}
diff --git a/base/src/com/google/idea/blaze/base/sync/sharding/WildcardTargetPattern.java b/base/src/com/google/idea/blaze/base/sync/sharding/WildcardTargetPattern.java
new file mode 100644
index 0000000..663825c
--- /dev/null
+++ b/base/src/com/google/idea/blaze/base/sync/sharding/WildcardTargetPattern.java
@@ -0,0 +1,134 @@
+/*
+ * Copyright 2017 The Bazel Authors. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.idea.blaze.base.sync.sharding;
+
+import com.google.common.collect.ImmutableList;
+import com.google.idea.blaze.base.model.primitives.TargetExpression;
+import com.google.idea.blaze.base.model.primitives.WorkspacePath;
+import com.intellij.openapi.util.io.FileUtil;
+import com.intellij.openapi.util.text.StringUtil;
+import javax.annotation.Nullable;
+
+/** A blaze wildcard target pattern. */
+public class WildcardTargetPattern {
+
+  private static final String ALL_PACKAGES_RECURSIVE_SUFFIX = "/...";
+  private static final ImmutableList<String> ALL_TARGETS_IN_SUFFIXES =
+      ImmutableList.of("*", "all-targets");
+  private static final String ALL_RULES_IN_SUFFIX = "all";
+
+  /** Returns null if the target is not a valid wildcard target pattern. */
+  @Nullable
+  public static WildcardTargetPattern fromExpression(TargetExpression target) {
+    String pattern = target.toString();
+    int colonIndex = pattern.lastIndexOf(':');
+    String packagePart = colonIndex < 0 ? pattern : pattern.substring(0, colonIndex);
+    String targetPart = colonIndex < 0 ? "" : pattern.substring(colonIndex + 1);
+
+    if (packagePart.startsWith("-")) {
+      packagePart = packagePart.substring(1);
+    }
+    packagePart = StringUtil.trimStart(packagePart, "//");
+
+    if (packagePart.endsWith(ALL_PACKAGES_RECURSIVE_SUFFIX)) {
+      WorkspacePath basePackageDir =
+          WorkspacePath.createIfValid(
+              StringUtil.trimEnd(packagePart, ALL_PACKAGES_RECURSIVE_SUFFIX));
+      if (basePackageDir == null) {
+        return null;
+      }
+      if (targetPart.isEmpty() || targetPart.equals(ALL_RULES_IN_SUFFIX)) {
+        return new WildcardTargetPattern(target, basePackageDir, true, true);
+      }
+      if (ALL_TARGETS_IN_SUFFIXES.contains(targetPart)) {
+        return new WildcardTargetPattern(target, basePackageDir, true, false);
+      }
+      return null; // ignore invalid patterns -- blaze will give us a better error later.
+    }
+
+    WorkspacePath packageDir = WorkspacePath.createIfValid(packagePart);
+    if (packageDir == null) {
+      return null;
+    }
+    if (targetPart.equals(ALL_RULES_IN_SUFFIX)) {
+      return new WildcardTargetPattern(target, packageDir, false, true);
+    }
+    if (ALL_TARGETS_IN_SUFFIXES.contains(targetPart)) {
+      return new WildcardTargetPattern(target, packageDir, false, false);
+    }
+    // not a wildcard target pattern
+    return null;
+  }
+
+  public final TargetExpression originalPattern;
+  private final WorkspacePath packageDir;
+  private final boolean recursive;
+  private final boolean rulesOnly;
+
+  private WildcardTargetPattern(
+      TargetExpression originalPattern,
+      WorkspacePath packageDir,
+      boolean recursive,
+      boolean rulesOnly) {
+    this.originalPattern = originalPattern;
+    this.packageDir = packageDir;
+    this.recursive = recursive;
+    this.rulesOnly = rulesOnly;
+  }
+
+  /** The base blaze package this target pattern refers to */
+  public WorkspacePath getBasePackage() {
+    return packageDir;
+  }
+
+  /** Whether the target pattern includes all packages below the base package. */
+  public boolean isRecursive() {
+    return recursive;
+  }
+
+  /** Whether the target pattern includes all targets, or only rules */
+  public boolean rulesOnly() {
+    return rulesOnly;
+  }
+
+  public boolean coversPackage(WorkspacePath packagePath) {
+    if (!recursive) {
+      return packagePath.equals(packageDir);
+    }
+    return FileUtil.isAncestor(packageDir.relativePath(), packagePath.relativePath(), false);
+  }
+
+  /** Is this an excluded target pattern (i.e. starts with '-')? */
+  public boolean isExcluded() {
+    return originalPattern.isExcluded();
+  }
+
+  @Override
+  public String toString() {
+    return originalPattern.toString();
+  }
+
+  @Override
+  public boolean equals(Object obj) {
+    return obj instanceof WildcardTargetPattern
+        && originalPattern.equals(((WildcardTargetPattern) obj).originalPattern);
+  }
+
+  @Override
+  public int hashCode() {
+    return originalPattern.hashCode();
+  }
+}
diff --git a/base/src/com/google/idea/blaze/base/sync/status/BlazeSyncStatusImpl.java b/base/src/com/google/idea/blaze/base/sync/status/BlazeSyncStatusImpl.java
index ee60cd7..5f74602 100644
--- a/base/src/com/google/idea/blaze/base/sync/status/BlazeSyncStatusImpl.java
+++ b/base/src/com/google/idea/blaze/base/sync/status/BlazeSyncStatusImpl.java
@@ -37,8 +37,8 @@
 import com.intellij.openapi.vfs.newvfs.NewVirtualFile;
 import java.util.Collection;
 import java.util.concurrent.atomic.AtomicBoolean;
+import javax.annotation.Nullable;
 import org.jetbrains.annotations.NotNull;
-import org.jetbrains.annotations.Nullable;
 
 /**
  * Per-project listener for changes to BUILD files, and other changes requiring an incremental sync.
diff --git a/base/src/com/google/idea/blaze/base/sync/workspace/ArtifactLocationDecoderImpl.java b/base/src/com/google/idea/blaze/base/sync/workspace/ArtifactLocationDecoderImpl.java
index 92ff6b8..1e3216a 100644
--- a/base/src/com/google/idea/blaze/base/sync/workspace/ArtifactLocationDecoderImpl.java
+++ b/base/src/com/google/idea/blaze/base/sync/workspace/ArtifactLocationDecoderImpl.java
@@ -15,30 +15,35 @@
  */
 package com.google.idea.blaze.base.sync.workspace;
 
+import com.google.idea.blaze.base.command.info.BlazeInfo;
 import com.google.idea.blaze.base.ideinfo.ArtifactLocation;
+import com.intellij.openapi.util.io.FileUtil;
 import java.io.File;
+import java.nio.file.Paths;
 
 /** Decodes intellij_ide_info.proto ArtifactLocation file paths */
 public class ArtifactLocationDecoderImpl implements ArtifactLocationDecoder {
   private static final long serialVersionUID = 1L;
 
-  private final BlazeRoots blazeRoots;
+  private final BlazeInfo blazeInfo;
   private final WorkspacePathResolver pathResolver;
 
-  public ArtifactLocationDecoderImpl(BlazeRoots blazeRoots, WorkspacePathResolver pathResolver) {
-    this.blazeRoots = blazeRoots;
+  public ArtifactLocationDecoderImpl(BlazeInfo blazeInfo, WorkspacePathResolver pathResolver) {
+    this.blazeInfo = blazeInfo;
     this.pathResolver = pathResolver;
   }
 
   @Override
   public File decode(ArtifactLocation artifactLocation) {
-    if (artifactLocation.isSource) {
-      if (artifactLocation.isExternal) {
-        return new File(blazeRoots.externalSourceRoot, artifactLocation.relativePath);
-      }
-      File root = pathResolver.findPackageRoot(artifactLocation.relativePath);
-      return new File(root, artifactLocation.relativePath);
+    if (artifactLocation.isSource && !artifactLocation.isExternal) {
+      return pathResolver.resolveToFile(artifactLocation.relativePath);
     }
-    return new File(blazeRoots.executionRoot, artifactLocation.getExecutionRootRelativePath());
+    String path =
+        Paths.get(
+                blazeInfo.getExecutionRoot().getPath(),
+                artifactLocation.getExecutionRootRelativePath())
+            .toString();
+    // doesn't require file-system operations -- no attempt to resolve symlinks.
+    return new File(FileUtil.toCanonicalPath(path));
   }
 }
diff --git a/base/src/com/google/idea/blaze/base/sync/workspace/BlazeRoots.java b/base/src/com/google/idea/blaze/base/sync/workspace/BlazeRoots.java
deleted file mode 100644
index c66d024..0000000
--- a/base/src/com/google/idea/blaze/base/sync/workspace/BlazeRoots.java
+++ /dev/null
@@ -1,128 +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.sync.workspace;
-
-import com.google.common.annotations.VisibleForTesting;
-import com.google.common.collect.ImmutableMap;
-import com.google.common.collect.Lists;
-import com.google.idea.blaze.base.command.info.BlazeInfo;
-import com.google.idea.blaze.base.io.FileAttributeProvider;
-import com.google.idea.blaze.base.model.primitives.ExecutionRootPath;
-import com.google.idea.blaze.base.model.primitives.WorkspaceRoot;
-import com.google.idea.blaze.base.settings.Blaze.BuildSystem;
-import com.intellij.openapi.diagnostic.Logger;
-import java.io.File;
-import java.io.Serializable;
-import java.util.List;
-
-/** The data output by BlazeInfo. */
-public class BlazeRoots implements Serializable {
-  public static final long serialVersionUID = 3L;
-  private static final Logger logger = Logger.getInstance(BlazeRoots.class);
-
-  public static BlazeRoots build(
-      BuildSystem buildSystem,
-      WorkspaceRoot workspaceRoot,
-      ImmutableMap<String, String> blazeInfo) {
-    return build(
-        workspaceRoot,
-        getOrThrow(buildSystem, blazeInfo, BlazeInfo.EXECUTION_ROOT_KEY),
-        getOrThrow(buildSystem, blazeInfo, BlazeInfo.PACKAGE_PATH_KEY),
-        getOrThrow(buildSystem, blazeInfo, BlazeInfo.blazeBinKey(buildSystem)),
-        getOrThrow(buildSystem, blazeInfo, BlazeInfo.blazeGenfilesKey(buildSystem)),
-        getOrThrow(buildSystem, blazeInfo, BlazeInfo.OUTPUT_BASE_KEY));
-  }
-
-  private static BlazeRoots build(
-      WorkspaceRoot workspaceRoot,
-      String execRootString,
-      String packagePathString,
-      String blazeBinRoot,
-      String blazeGenfilesRoot,
-      String externalSourceRoot) {
-    List<File> packagePaths = parsePackagePaths(workspaceRoot.toString(), packagePathString.trim());
-    File executionRoot = new File(execRootString.trim());
-    ExecutionRootPath blazeBinExecutionRootPath =
-        ExecutionRootPath.createAncestorRelativePath(executionRoot, new File(blazeBinRoot));
-    ExecutionRootPath blazeGenfilesExecutionRootPath =
-        ExecutionRootPath.createAncestorRelativePath(executionRoot, new File(blazeGenfilesRoot));
-    File externalSourceRootFile = new File(externalSourceRoot.trim());
-    logger.assertTrue(blazeBinExecutionRootPath != null);
-    logger.assertTrue(blazeGenfilesExecutionRootPath != null);
-    return new BlazeRoots(
-        executionRoot,
-        packagePaths,
-        blazeBinExecutionRootPath,
-        blazeGenfilesExecutionRootPath,
-        externalSourceRootFile);
-  }
-
-  private static String getOrThrow(
-      BuildSystem buildSystem, ImmutableMap<String, String> map, String key) {
-    String value = map.get(key);
-    if (value == null) {
-      throw new RuntimeException(
-          String.format("Could not locate %s in %s info", key, buildSystem.getLowerCaseName()));
-    }
-    return value;
-  }
-
-  private static List<File> parsePackagePaths(String workspaceRoot, String packagePathString) {
-    String[] paths = packagePathString.split(":");
-    List<File> packagePaths = Lists.newArrayListWithCapacity(paths.length);
-    FileAttributeProvider fileAttributeProvider = FileAttributeProvider.getInstance();
-    for (String path : paths) {
-      File packagePath = new File(path.replace("%workspace%", workspaceRoot));
-      if (fileAttributeProvider.exists(packagePath)) {
-        packagePaths.add(packagePath);
-      }
-    }
-    return packagePaths;
-  }
-
-  public final File executionRoot;
-  public final List<File> packagePaths;
-  public final ExecutionRootPath blazeBinExecutionRootPath;
-  public final ExecutionRootPath blazeGenfilesExecutionRootPath;
-  public final File externalSourceRoot;
-
-  @VisibleForTesting
-  public BlazeRoots(
-      File executionRoot,
-      List<File> packagePaths,
-      ExecutionRootPath blazeBinExecutionRootPath,
-      ExecutionRootPath blazeGenfilesExecutionRootPath,
-      File externalSourceRoot) {
-    this.executionRoot = executionRoot;
-    this.packagePaths = packagePaths;
-    this.blazeBinExecutionRootPath = blazeBinExecutionRootPath;
-    this.blazeGenfilesExecutionRootPath = blazeGenfilesExecutionRootPath;
-    this.externalSourceRoot = externalSourceRoot;
-  }
-
-  public File getGenfilesDirectory() {
-    return blazeGenfilesExecutionRootPath.getFileRootedAt(executionRoot);
-  }
-
-  public File getBlazeBinDirectory() {
-    return blazeBinExecutionRootPath.getFileRootedAt(executionRoot);
-  }
-
-  public boolean isOutputArtifact(ExecutionRootPath path) {
-    return ExecutionRootPath.isAncestor(blazeGenfilesExecutionRootPath, path, false)
-        || ExecutionRootPath.isAncestor(blazeBinExecutionRootPath, path, false);
-  }
-}
diff --git a/base/src/com/google/idea/blaze/base/sync/workspace/ExecutionRootPathResolver.java b/base/src/com/google/idea/blaze/base/sync/workspace/ExecutionRootPathResolver.java
index c6b7eef..c1b3f29 100644
--- a/base/src/com/google/idea/blaze/base/sync/workspace/ExecutionRootPathResolver.java
+++ b/base/src/com/google/idea/blaze/base/sync/workspace/ExecutionRootPathResolver.java
@@ -16,11 +16,12 @@
 package com.google.idea.blaze.base.sync.workspace;
 
 import com.google.common.collect.ImmutableList;
+import com.google.idea.blaze.base.bazel.BuildSystemProvider;
 import com.google.idea.blaze.base.model.primitives.ExecutionRootPath;
 import com.google.idea.blaze.base.model.primitives.WorkspacePath;
-import com.intellij.openapi.util.io.FileUtil;
+import com.google.idea.blaze.base.model.primitives.WorkspaceRoot;
+import com.google.idea.blaze.base.settings.Blaze.BuildSystem;
 import java.io.File;
-import java.util.List;
 
 /**
  * Converts execution-root-relative paths to absolute files with a minimum of file system calls
@@ -31,15 +32,29 @@
  */
 public class ExecutionRootPathResolver {
 
-  private final BlazeRoots blazeRoots;
+  private final ImmutableList<String> buildArtifactDirectories;
+  private final File executionRoot;
   private final WorkspacePathResolver workspacePathResolver;
 
   public ExecutionRootPathResolver(
-      BlazeRoots blazeRoots, WorkspacePathResolver workspacePathResolver) {
-    this.blazeRoots = blazeRoots;
+      BuildSystem buildSystem,
+      WorkspaceRoot workspaceRoot,
+      File executionRoot,
+      WorkspacePathResolver workspacePathResolver) {
+    this.buildArtifactDirectories = buildArtifactDirectories(buildSystem, workspaceRoot);
+    this.executionRoot = executionRoot;
     this.workspacePathResolver = workspacePathResolver;
   }
 
+  private static ImmutableList<String> buildArtifactDirectories(
+      BuildSystem buildSystem, WorkspaceRoot workspaceRoot) {
+    BuildSystemProvider provider = BuildSystemProvider.getBuildSystemProvider(buildSystem);
+    if (provider == null) {
+      provider = BuildSystemProvider.defaultBuildSystem();
+    }
+    return provider.buildArtifactDirectories(workspaceRoot);
+  }
+
   /**
    * This method should be used for directories. Returns all workspace files corresponding to the
    * given execution-root-relative path. If the file does not exist inside the workspace (e.g. for
@@ -53,19 +68,21 @@
       WorkspacePath workspacePath = new WorkspacePath(path.getAbsoluteOrRelativeFile().getPath());
       return workspacePathResolver.resolveToIncludeDirectories(workspacePath);
     }
-    return ImmutableList.of(path.getFileRootedAt(blazeRoots.executionRoot));
+    return ImmutableList.of(path.getFileRootedAt(executionRoot));
   }
 
   private boolean isInWorkspace(ExecutionRootPath path) {
-    boolean inOutputDir =
-        ExecutionRootPath.isAncestor(blazeRoots.blazeBinExecutionRootPath, path, false)
-            || ExecutionRootPath.isAncestor(blazeRoots.blazeGenfilesExecutionRootPath, path, false)
-            || isExternalWorkspacePath(path);
-    return !inOutputDir;
+    String firstPathComponent = getFirstPathComponent(path.getAbsoluteOrRelativeFile().getPath());
+    return !buildArtifactDirectories.contains(firstPathComponent)
+        && !isExternalWorkspacePath(firstPathComponent);
   }
 
-  private static boolean isExternalWorkspacePath(ExecutionRootPath path) {
-    List<String> pathComponents = FileUtil.splitPath(path.getAbsoluteOrRelativeFile().getPath());
-    return pathComponents.size() > 1 && "external".equals(pathComponents.get(0));
+  private static String getFirstPathComponent(String path) {
+    int index = path.indexOf(File.separatorChar);
+    return index == -1 ? path : path.substring(0, index);
+  }
+
+  private static boolean isExternalWorkspacePath(String firstPathComponent) {
+    return firstPathComponent.equals("external");
   }
 }
diff --git a/base/src/com/google/idea/blaze/base/sync/workspace/WorkspaceHelper.java b/base/src/com/google/idea/blaze/base/sync/workspace/WorkspaceHelper.java
new file mode 100644
index 0000000..81ade09
--- /dev/null
+++ b/base/src/com/google/idea/blaze/base/sync/workspace/WorkspaceHelper.java
@@ -0,0 +1,183 @@
+/*
+ * Copyright 2017 The Bazel Authors. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.idea.blaze.base.sync.workspace;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.collect.ImmutableMap;
+import com.google.idea.blaze.base.bazel.BuildSystemProvider;
+import com.google.idea.blaze.base.io.FileAttributeProvider;
+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.TargetName;
+import com.google.idea.blaze.base.model.primitives.WorkspacePath;
+import com.google.idea.blaze.base.model.primitives.WorkspaceRoot;
+import com.google.idea.blaze.base.settings.Blaze;
+import com.google.idea.blaze.base.settings.Blaze.BuildSystem;
+import com.google.idea.blaze.base.sync.SyncCache;
+import com.intellij.openapi.project.Project;
+import com.intellij.openapi.util.io.FileUtil;
+import java.io.File;
+import java.util.Arrays;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.stream.Collectors;
+import javax.annotation.Nullable;
+
+/** External-workspace-aware resolution of workspace paths. */
+public class WorkspaceHelper {
+
+  private static class Workspace {
+    private final WorkspaceRoot root;
+    @Nullable private final String externalWorkspaceName;
+
+    private Workspace(WorkspaceRoot root, @Nullable String externalWorkspaceName) {
+      this.root = root;
+      this.externalWorkspaceName = externalWorkspaceName;
+    }
+  }
+
+  @Nullable
+  public static WorkspaceRoot resolveExternalWorkspace(Project project, String workspaceName) {
+    Map<String, WorkspaceRoot> externalWorkspaces = getExternalWorkspaceRoots(project);
+    return externalWorkspaces != null ? externalWorkspaces.get(workspaceName) : null;
+  }
+
+  /** Resolves the parent blaze package corresponding to this label. */
+  @Nullable
+  public static File resolveBlazePackage(Project project, Label label) {
+    if (!label.isExternal()) {
+      WorkspacePathResolver pathResolver =
+          WorkspacePathResolverProvider.getInstance(project).getPathResolver();
+      return pathResolver != null ? pathResolver.resolveToFile(label.blazePackage()) : null;
+    }
+    Map<String, WorkspaceRoot> externalWorkspaces = getExternalWorkspaceRoots(project);
+    if (externalWorkspaces == null) {
+      return null;
+    }
+    WorkspaceRoot root = externalWorkspaces.get(label.externalWorkspaceName());
+    return root != null ? root.fileForPath(label.blazePackage()) : null;
+  }
+
+  @Nullable
+  public static WorkspacePath resolveWorkspacePath(Project project, File absoluteFile) {
+    Workspace workspace = resolveWorkspace(project, absoluteFile);
+    return workspace != null ? workspace.root.workspacePathForSafe(absoluteFile) : null;
+  }
+
+  /** Converts a file to the corresponding BUILD label for this project, if valid. */
+  @Nullable
+  public static Label getBuildLabel(Project project, File absoluteFile) {
+    Workspace workspace = resolveWorkspace(project, absoluteFile);
+    if (workspace == null) {
+      return null;
+    }
+    WorkspacePath workspacePath = workspace.root.workspacePathForSafe(absoluteFile);
+    if (workspacePath == null) {
+      return null;
+    }
+    return deriveLabel(project, workspace, workspacePath);
+  }
+
+  @Nullable
+  private static Workspace resolveWorkspace(Project project, File absoluteFile) {
+    WorkspacePathResolver pathResolver =
+        WorkspacePathResolverProvider.getInstance(project).getPathResolver();
+    if (pathResolver == null) {
+      return null;
+    }
+
+    // try project workspace first
+    WorkspaceRoot root = pathResolver.findWorkspaceRoot(absoluteFile);
+    if (root != null) {
+      return new Workspace(root, null);
+    }
+
+    Map<String, WorkspaceRoot> externalWorkspaces = getExternalWorkspaceRoots(project);
+    if (externalWorkspaces == null) {
+      return null;
+    }
+    for (Entry<String, WorkspaceRoot> entry : externalWorkspaces.entrySet()) {
+      root = entry.getValue();
+      WorkspacePath workspacePath = root.workspacePathForSafe(absoluteFile);
+      if (workspacePath != null) {
+        return new Workspace(root, entry.getKey());
+      }
+    }
+    return null;
+  }
+
+  private static Label deriveLabel(
+      Project project, Workspace workspace, WorkspacePath workspacePath) {
+    BuildSystemProvider provider = Blaze.getBuildSystemProvider(project);
+    File file = workspace.root.fileForPath(workspacePath);
+    if (provider.isBuildFile(file.getName())) {
+      return Label.create(
+          workspace.externalWorkspaceName,
+          workspace.root.workspacePathFor(file.getParentFile()),
+          TargetName.create("__pkg__"));
+    }
+    WorkspacePath packagePath = getPackagePath(provider, workspace.root, workspacePath);
+    if (packagePath == null) {
+      return null;
+    }
+    TargetName targetName =
+        TargetName.createIfValid(
+            FileUtil.getRelativePath(workspace.root.fileForPath(packagePath), file));
+    return targetName != null
+        ? Label.create(workspace.externalWorkspaceName, packagePath, targetName)
+        : null;
+  }
+
+  private static WorkspacePath getPackagePath(
+      BuildSystemProvider provider, WorkspaceRoot root, WorkspacePath workspacePath) {
+    File file = root.fileForPath(workspacePath).getParentFile();
+    while (file != null && FileUtil.isAncestor(root.directory(), file, false)) {
+      if (provider.findBuildFileInDirectory(file) != null) {
+        return root.workspacePathFor(file);
+      }
+      file = file.getParentFile();
+    }
+    return null;
+  }
+
+  @Nullable
+  private static synchronized Map<String, WorkspaceRoot> getExternalWorkspaceRoots(
+      Project project) {
+    if (Blaze.getBuildSystem(project) == BuildSystem.Blaze) {
+      return ImmutableMap.of();
+    }
+    return SyncCache.getInstance(project)
+        .get(WorkspaceHelper.class, WorkspaceHelper::enumerateExternalWorkspaces);
+  }
+
+  @SuppressWarnings("unused")
+  private static Map<String, WorkspaceRoot> enumerateExternalWorkspaces(
+      Project project, BlazeProjectData blazeProjectData) {
+    FileAttributeProvider provider = FileAttributeProvider.getInstance();
+    File[] children = provider.listFiles(getExternalSourceRoot(blazeProjectData));
+    if (children == null) {
+      return ImmutableMap.of();
+    }
+    return Arrays.stream(children)
+        .filter(provider::isDirectory)
+        .collect(Collectors.toMap(File::getName, WorkspaceRoot::new));
+  }
+
+  @VisibleForTesting
+  public static File getExternalSourceRoot(BlazeProjectData projectData) {
+    return new File(projectData.blazeInfo.getOutputBase(), "external");
+  }
+}
diff --git a/base/src/com/google/idea/blaze/base/sync/workspace/WorkspacePathResolver.java b/base/src/com/google/idea/blaze/base/sync/workspace/WorkspacePathResolver.java
index 128ca91..7392630 100644
--- a/base/src/com/google/idea/blaze/base/sync/workspace/WorkspacePathResolver.java
+++ b/base/src/com/google/idea/blaze/base/sync/workspace/WorkspacePathResolver.java
@@ -17,6 +17,7 @@
 
 import com.google.common.collect.ImmutableList;
 import com.google.idea.blaze.base.model.primitives.WorkspacePath;
+import com.google.idea.blaze.base.model.primitives.WorkspaceRoot;
 import java.io.File;
 import java.io.Serializable;
 import javax.annotation.Nullable;
@@ -47,6 +48,13 @@
   File findPackageRoot(String relativePath);
 
   /**
+   * Finds the workspace root directory that an absolute file lies under. Returns null if the file
+   * is not in a known workspace.
+   */
+  @Nullable
+  WorkspaceRoot findWorkspaceRoot(File absoluteFile);
+
+  /**
    * Given a resolved, absolute file, returns the corresponding {@link WorkspacePath}. Returns null
    * if the file is not in the workspace.
    */
diff --git a/base/src/com/google/idea/blaze/base/sync/workspace/WorkspacePathResolverImpl.java b/base/src/com/google/idea/blaze/base/sync/workspace/WorkspacePathResolverImpl.java
index ac131c1..bccb969 100644
--- a/base/src/com/google/idea/blaze/base/sync/workspace/WorkspacePathResolverImpl.java
+++ b/base/src/com/google/idea/blaze/base/sync/workspace/WorkspacePathResolverImpl.java
@@ -16,31 +16,19 @@
 package com.google.idea.blaze.base.sync.workspace;
 
 import com.google.common.collect.ImmutableList;
-import com.google.idea.blaze.base.io.FileAttributeProvider;
 import com.google.idea.blaze.base.model.primitives.WorkspacePath;
 import com.google.idea.blaze.base.model.primitives.WorkspaceRoot;
 import java.io.File;
-import java.util.List;
 import javax.annotation.Nullable;
 
 /** Uses the package path locations to resolve a workspace path. */
 public class WorkspacePathResolverImpl implements WorkspacePathResolver {
-  private static final long serialVersionUID = 2L;
+  private static final long serialVersionUID = 3L;
 
   private final WorkspaceRoot workspaceRoot;
-  private final List<File> packagePaths;
-
-  public WorkspacePathResolverImpl(WorkspaceRoot workspaceRoot, BlazeRoots blazeRoots) {
-    this(workspaceRoot, blazeRoots.packagePaths);
-  }
 
   public WorkspacePathResolverImpl(WorkspaceRoot workspaceRoot) {
-    this(workspaceRoot, ImmutableList.of(workspaceRoot.directory()));
-  }
-
-  public WorkspacePathResolverImpl(WorkspaceRoot workspaceRoot, List<File> packagePaths) {
     this.workspaceRoot = workspaceRoot;
-    this.packagePaths = packagePaths;
   }
 
   @Override
@@ -50,19 +38,7 @@
 
   @Override
   public File findPackageRoot(String relativePath) {
-    if (packagePaths.size() == 1) {
-      return packagePaths.get(0);
-    }
-    // fall back to manually checking each one
-    FileAttributeProvider existenceChecker = FileAttributeProvider.getInstance();
-    for (File pkg : packagePaths) {
-      if (existenceChecker.exists(new File(pkg, relativePath))) {
-        return pkg;
-      }
-    }
-
-    // Return first in package path, even though it might not exist
-    return packagePaths.get(0);
+    return workspaceRoot.directory();
   }
 
   @Nullable
@@ -70,4 +46,10 @@
   public WorkspacePath getWorkspacePath(File absoluteFile) {
     return workspaceRoot.workspacePathForSafe(absoluteFile);
   }
+
+  @Nullable
+  @Override
+  public WorkspaceRoot findWorkspaceRoot(File absoluteFile) {
+    return workspaceRoot.isInWorkspace(absoluteFile) ? workspaceRoot : null;
+  }
 }
diff --git a/base/src/com/google/idea/blaze/base/targetmaps/SourceToTargetMapImpl.java b/base/src/com/google/idea/blaze/base/targetmaps/SourceToTargetMapImpl.java
index 4d4a28f..19e8383 100644
--- a/base/src/com/google/idea/blaze/base/targetmaps/SourceToTargetMapImpl.java
+++ b/base/src/com/google/idea/blaze/base/targetmaps/SourceToTargetMapImpl.java
@@ -74,7 +74,7 @@
         .get(SourceToTargetMapImpl.class, SourceToTargetMapImpl::computeSourceToTargetMap);
   }
 
-  @Nullable
+  @SuppressWarnings("unused")
   private static ImmutableMultimap<File, TargetKey> computeSourceToTargetMap(
       Project project, BlazeProjectData blazeProjectData) {
     ArtifactLocationDecoder artifactLocationDecoder = blazeProjectData.artifactLocationDecoder;
diff --git a/base/src/com/google/idea/blaze/base/targetmaps/TransitiveDependencyMap.java b/base/src/com/google/idea/blaze/base/targetmaps/TransitiveDependencyMap.java
index f973c8d..b2a31ba 100644
--- a/base/src/com/google/idea/blaze/base/targetmaps/TransitiveDependencyMap.java
+++ b/base/src/com/google/idea/blaze/base/targetmaps/TransitiveDependencyMap.java
@@ -16,6 +16,7 @@
 package com.google.idea.blaze.base.targetmaps;
 
 import com.google.common.collect.ImmutableCollection;
+import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableSet;
 import com.google.common.collect.Queues;
 import com.google.common.collect.Sets;
@@ -26,6 +27,7 @@
 import com.google.idea.blaze.base.sync.data.BlazeProjectDataManager;
 import com.intellij.openapi.components.ServiceManager;
 import com.intellij.openapi.project.Project;
+import java.util.Collection;
 import java.util.List;
 import java.util.Queue;
 import java.util.Set;
@@ -53,11 +55,16 @@
     return getTransitiveDependencies(targetKey, blazeProjectData.targetMap);
   }
 
-  private static ImmutableCollection<TargetKey> getTransitiveDependencies(
+  public static ImmutableCollection<TargetKey> getTransitiveDependencies(
       TargetKey targetKey, TargetMap targetMap) {
+    return getTransitiveDependencies(ImmutableList.of(targetKey), targetMap);
+  }
+
+  public static ImmutableCollection<TargetKey> getTransitiveDependencies(
+      Collection<TargetKey> targetKeys, TargetMap targetMap) {
     Queue<TargetKey> targetsToVisit = Queues.newArrayDeque();
     Set<TargetKey> transitiveDependencies = Sets.newHashSet();
-    targetsToVisit.add(targetKey);
+    targetsToVisit.addAll(targetKeys);
     while (!targetsToVisit.isEmpty()) {
       TargetIdeInfo currentTarget = targetMap.get(targetsToVisit.remove());
       if (currentTarget == null) {
diff --git a/base/src/com/google/idea/blaze/base/treeview/BlazeTreeStructureProvider.java b/base/src/com/google/idea/blaze/base/treeview/BlazeTreeStructureProvider.java
index 764e3b0..712c1fc 100644
--- a/base/src/com/google/idea/blaze/base/treeview/BlazeTreeStructureProvider.java
+++ b/base/src/com/google/idea/blaze/base/treeview/BlazeTreeStructureProvider.java
@@ -36,8 +36,8 @@
 import java.io.File;
 import java.util.Collection;
 import java.util.List;
+import javax.annotation.Nullable;
 import org.jetbrains.annotations.NotNull;
-import org.jetbrains.annotations.Nullable;
 
 /**
  * Modifies the project view:
@@ -147,6 +147,9 @@
 
       @Override
       public boolean isShowExcludedFiles() {
+        if (original instanceof ProjectViewSettings) {
+          return ((ProjectViewSettings) original).isShowExcludedFiles();
+        }
         return true;
       }
     };
diff --git a/base/src/com/google/idea/blaze/base/ui/BlazeValidationError.java b/base/src/com/google/idea/blaze/base/ui/BlazeValidationError.java
index 452ae82..f79fffc 100644
--- a/base/src/com/google/idea/blaze/base/ui/BlazeValidationError.java
+++ b/base/src/com/google/idea/blaze/base/ui/BlazeValidationError.java
@@ -18,9 +18,9 @@
 import com.intellij.openapi.project.Project;
 import com.intellij.openapi.ui.Messages;
 import java.util.Collection;
+import javax.annotation.Nullable;
 import javax.annotation.concurrent.Immutable;
 import org.jetbrains.annotations.NotNull;
-import org.jetbrains.annotations.Nullable;
 
 /** An error occuring during a blaze validation */
 @Immutable
diff --git a/base/src/com/google/idea/blaze/base/ui/BlazeValidationResult.java b/base/src/com/google/idea/blaze/base/ui/BlazeValidationResult.java
index 4200003..b6bc270 100644
--- a/base/src/com/google/idea/blaze/base/ui/BlazeValidationResult.java
+++ b/base/src/com/google/idea/blaze/base/ui/BlazeValidationResult.java
@@ -15,7 +15,7 @@
  */
 package com.google.idea.blaze.base.ui;
 
-import org.jetbrains.annotations.Nullable;
+import javax.annotation.Nullable;
 
 /** Pair of (success, validation error) */
 public class BlazeValidationResult {
diff --git a/base/src/com/google/idea/blaze/base/ui/WorkspaceFileTextField.java b/base/src/com/google/idea/blaze/base/ui/WorkspaceFileTextField.java
new file mode 100644
index 0000000..87a45a0
--- /dev/null
+++ b/base/src/com/google/idea/blaze/base/ui/WorkspaceFileTextField.java
@@ -0,0 +1,129 @@
+/*
+ * Copyright 2017 The Bazel Authors. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.idea.blaze.base.ui;
+
+import com.google.common.collect.ImmutableMap;
+import com.google.idea.blaze.base.model.primitives.WorkspacePath;
+import com.google.idea.blaze.base.sync.workspace.WorkspacePathResolver;
+import com.intellij.openapi.Disposable;
+import com.intellij.openapi.fileChooser.FileChooserDescriptor;
+import com.intellij.openapi.fileChooser.FileTextField;
+import com.intellij.openapi.fileChooser.ex.FileTextFieldImpl;
+import com.intellij.openapi.fileChooser.ex.LocalFsFinder.FileChooserFilter;
+import com.intellij.openapi.fileChooser.ex.LocalFsFinder.IoFile;
+import com.intellij.openapi.fileChooser.ex.LocalFsFinder.VfsFile;
+import com.intellij.openapi.vfs.LocalFileSystem;
+import com.intellij.openapi.vfs.VirtualFile;
+import com.intellij.ui.components.JBTextField;
+import java.io.File;
+import javax.annotation.Nullable;
+import javax.swing.JTextField;
+
+/** A text field that auto-completes paths in the workspace root. */
+// This outer class basically a copy of FileTextFieldImpl.Vfs but we can't use that directly because
+// we need to specify our own custom Finder.
+public final class WorkspaceFileTextField extends FileTextFieldImpl {
+
+  private WorkspaceFileTextField(
+      WorkspacePathResolver pathResolver,
+      JTextField textField,
+      LookupFilter filter,
+      Disposable parent) {
+    super(textField, new WorkspaceFinder(pathResolver), filter, ImmutableMap.of(), parent);
+  }
+
+  public static FileTextField create(
+      WorkspacePathResolver pathResolver,
+      FileChooserDescriptor descriptor,
+      int columns,
+      Disposable parent) {
+    JTextField textField = new WorkspacePathTextField(pathResolver, columns);
+    return new WorkspaceFileTextField(
+        pathResolver, textField, new FileChooserFilter(descriptor, /* showHidden */ true), parent);
+  }
+
+  @Nullable
+  @Override
+  public VirtualFile getSelectedFile() {
+    LookupFile lookupFile = getFile();
+    return lookupFile != null ? ((VfsFile) lookupFile).getFile() : null;
+  }
+
+  private static class WorkspaceFinder implements Finder {
+
+    private final WorkspacePathResolver pathResolver;
+
+    private WorkspaceFinder(WorkspacePathResolver pathResolver) {
+      this.pathResolver = pathResolver;
+    }
+
+    @Nullable
+    @Override
+    public LookupFile find(String path) {
+      File file = new File(normalize(path));
+      VirtualFile vFile = LocalFileSystem.getInstance().findFileByIoFile(file);
+      if (vFile != null) {
+        return new VfsFile(/* unused LocalFsFinder */ null, vFile);
+      } else if (file.isAbsolute()) {
+        return new IoFile(new File(path));
+      }
+      return null;
+    }
+
+    @Override
+    public String normalize(String path) {
+      File file = new File(path);
+      if (!file.isAbsolute()) {
+        file = pathResolver.resolveToFile(path);
+      }
+
+      return file.getAbsolutePath();
+    }
+
+    @Override
+    public String getSeparator() {
+      return File.separator;
+    }
+  }
+
+  // FileTextFieldImpl calls setText with the absolute path after every filename autocomplete. But
+  // we don't want to swap in the absolute path, we'd rather just show the path relative to the
+  // workspace root. So we override setText().
+  private static class WorkspacePathTextField extends JBTextField {
+
+    final WorkspacePathResolver pathResolver;
+
+    WorkspacePathTextField(WorkspacePathResolver pathResolver, int columns) {
+      super(columns);
+      this.pathResolver = pathResolver;
+    }
+
+    @Override
+    public void setText(String path) {
+      WorkspacePath workspacePath = pathResolver.getWorkspacePath(new File(path));
+      if (workspacePath == null) {
+        super.setText(path);
+        return;
+      }
+
+      String relativePath = workspacePath.relativePath();
+      if (path.endsWith(File.separator) && !relativePath.endsWith(File.separator)) {
+        relativePath += File.separator;
+      }
+      super.setText(relativePath);
+    }
+  }
+}
diff --git a/base/src/com/google/idea/blaze/base/util/PackagePrefixCalculator.java b/base/src/com/google/idea/blaze/base/util/PackagePrefixCalculator.java
index e923236..f4bbfda 100644
--- a/base/src/com/google/idea/blaze/base/util/PackagePrefixCalculator.java
+++ b/base/src/com/google/idea/blaze/base/util/PackagePrefixCalculator.java
@@ -24,9 +24,16 @@
   public static String packagePrefixOf(@NotNull WorkspacePath workspacePath) {
     int skipIndex = 0;
 
+    // For Bazel-style projects.
     skipIndex = skipIndex == 0 ? skip(workspacePath, "java/") : skipIndex;
     skipIndex = skipIndex == 0 ? skip(workspacePath, "javatests/") : skipIndex;
 
+    // For Maven-style projects.
+    skipIndex = skipIndex == 0 ? skip(workspacePath, "src/main/java/") : skipIndex;
+    skipIndex = skipIndex == 0 ? skip(workspacePath, "src/test/java/") : skipIndex;
+    skipIndex = skipIndex == 0 ? skip(workspacePath, "src/main/scala/") : skipIndex;
+    skipIndex = skipIndex == 0 ? skip(workspacePath, "src/test/scala/") : skipIndex;
+
     return workspacePath.relativePath().substring(skipIndex).replace('/', '.');
   }
 
diff --git a/base/src/com/google/idea/blaze/base/util/SerializationUtil.java b/base/src/com/google/idea/blaze/base/util/SerializationUtil.java
index f098673..ea689da 100644
--- a/base/src/com/google/idea/blaze/base/util/SerializationUtil.java
+++ b/base/src/com/google/idea/blaze/base/util/SerializationUtil.java
@@ -17,7 +17,6 @@
 
 import com.google.common.io.Closeables;
 import com.intellij.CommonBundle;
-import com.intellij.openapi.diagnostic.Logger;
 import java.io.File;
 import java.io.FileInputStream;
 import java.io.FileOutputStream;
@@ -26,12 +25,11 @@
 import java.io.ObjectOutputStream;
 import java.io.ObjectStreamClass;
 import java.io.Serializable;
+import javax.annotation.Nullable;
 import org.jetbrains.annotations.NotNull;
-import org.jetbrains.annotations.Nullable;
 
 /** Utils for serialization. */
 public class SerializationUtil {
-  private static final Logger logger = Logger.getInstance(SerializationUtil.class.getName());
 
   public static void saveToDisk(@NotNull File file, @NotNull Serializable serializable)
       throws IOException {
diff --git a/base/src/com/google/idea/blaze/base/wizard2/BlazeNewProjectBuilder.java b/base/src/com/google/idea/blaze/base/wizard2/BlazeNewProjectBuilder.java
index 16648e6..578af51 100644
--- a/base/src/com/google/idea/blaze/base/wizard2/BlazeNewProjectBuilder.java
+++ b/base/src/com/google/idea/blaze/base/wizard2/BlazeNewProjectBuilder.java
@@ -36,7 +36,6 @@
 import java.io.IOException;
 import java.util.Arrays;
 import java.util.List;
-import java.util.UUID;
 
 /** Contains the state to build a new project throughout the new project wizard process. */
 public final class BlazeNewProjectBuilder {
@@ -218,7 +217,6 @@
             workspaceRoot.directory().getPath(),
             projectName,
             projectDataDirectory,
-            createLocationHash(projectName),
             projectViewFile.getPath(),
             buildSystem);
 
@@ -226,10 +224,4 @@
     PluginDependencyHelper.addDependencyOnSyncPlugin(project);
     // Initial sync of the project happens in BlazeSyncStartupActivity
   }
-
-  private static String createLocationHash(String projectName) {
-    String uuid = UUID.randomUUID().toString();
-    uuid = uuid.substring(0, Math.min(uuid.length(), 8));
-    return projectName.replaceAll("[^a-zA-Z0-9]", "") + "-" + uuid;
-  }
 }
diff --git a/base/src/com/google/idea/blaze/base/wizard2/BlazeSelectProjectViewOption.java b/base/src/com/google/idea/blaze/base/wizard2/BlazeSelectProjectViewOption.java
index 353ecb4..51229f0 100644
--- a/base/src/com/google/idea/blaze/base/wizard2/BlazeSelectProjectViewOption.java
+++ b/base/src/com/google/idea/blaze/base/wizard2/BlazeSelectProjectViewOption.java
@@ -37,9 +37,9 @@
     return false;
   }
 
-  /** Returns the default project name */
-  default String getDefaultProjectName(String workspaceName) {
-    return workspaceName;
+  /** Returns the directory we're importing from, if applicable. */
+  default String getImportDirectory() {
+    return null;
   }
 
   void commit();
diff --git a/base/src/com/google/idea/blaze/base/wizard2/BlazeSelectWorkspaceOption.java b/base/src/com/google/idea/blaze/base/wizard2/BlazeSelectWorkspaceOption.java
index 11bf115..3c9a601 100644
--- a/base/src/com/google/idea/blaze/base/wizard2/BlazeSelectWorkspaceOption.java
+++ b/base/src/com/google/idea/blaze/base/wizard2/BlazeSelectWorkspaceOption.java
@@ -34,6 +34,9 @@
   /** @return the name of the workspace. Used to generate default project names. */
   String getWorkspaceName();
 
+  /** @return the name of the 'branch', if applicable */
+  String getBranchName();
+
   BuildSystem getBuildSystemForWorkspace();
 
   void commit() throws BlazeProjectCommitException;
diff --git a/base/src/com/google/idea/blaze/base/wizard2/BlazeWizardUserSettingsStorage.java b/base/src/com/google/idea/blaze/base/wizard2/BlazeWizardUserSettingsStorage.java
index 2184d14..0ff0440 100644
--- a/base/src/com/google/idea/blaze/base/wizard2/BlazeWizardUserSettingsStorage.java
+++ b/base/src/com/google/idea/blaze/base/wizard2/BlazeWizardUserSettingsStorage.java
@@ -19,7 +19,7 @@
 import com.intellij.openapi.components.ServiceManager;
 import com.intellij.openapi.components.State;
 import com.intellij.openapi.components.Storage;
-import org.jetbrains.annotations.Nullable;
+import javax.annotation.Nullable;
 
 /** Stores wizard user settings between runs. */
 @State(name = "BlazeWizardUserSettings", storages = @Storage("blaze.wizard.settings.xml"))
diff --git a/base/src/com/google/idea/blaze/base/wizard2/GenerateFromBuildFileSelectProjectViewOption.java b/base/src/com/google/idea/blaze/base/wizard2/GenerateFromBuildFileSelectProjectViewOption.java
index 341d023..577f290 100644
--- a/base/src/com/google/idea/blaze/base/wizard2/GenerateFromBuildFileSelectProjectViewOption.java
+++ b/base/src/com/google/idea/blaze/base/wizard2/GenerateFromBuildFileSelectProjectViewOption.java
@@ -15,6 +15,8 @@
  */
 package com.google.idea.blaze.base.wizard2;
 
+import static com.google.common.base.Preconditions.checkState;
+
 import com.google.common.base.Strings;
 import com.google.idea.blaze.base.bazel.BuildSystemProvider;
 import com.google.idea.blaze.base.model.primitives.TargetExpression;
@@ -27,6 +29,7 @@
 import com.google.idea.blaze.base.projectview.section.sections.TargetSection;
 import com.google.idea.blaze.base.projectview.section.sections.TextBlock;
 import com.google.idea.blaze.base.projectview.section.sections.TextBlockSection;
+import com.google.idea.blaze.base.sync.projectview.RelatedWorkspacePathFinder;
 import com.google.idea.blaze.base.sync.workspace.WorkspacePathResolver;
 import com.google.idea.blaze.base.ui.BlazeValidationResult;
 import com.google.idea.blaze.base.ui.UiUtil;
@@ -41,6 +44,8 @@
 import com.intellij.ui.TextFieldWithStoredHistory;
 import java.awt.Dimension;
 import java.io.File;
+import java.util.ArrayList;
+import java.util.List;
 import javax.annotation.Nullable;
 import javax.swing.JButton;
 import javax.swing.JComponent;
@@ -91,15 +96,26 @@
 
   @Override
   public BlazeValidationResult validate() {
-    if (getBuildFilePath().isEmpty()) {
+    String buildFilePath = getBuildFilePath();
+    if (buildFilePath.isEmpty()) {
       return BlazeValidationResult.failure("BUILD file field cannot be empty.");
     }
+    if (!WorkspacePath.validate(buildFilePath)) {
+      return BlazeValidationResult.failure(
+          "Invalid BUILD file path: specify a path relative to the workspace root.");
+    }
     WorkspacePathResolver workspacePathResolver =
         builder.getWorkspaceOption().getWorkspacePathResolver();
-    File file = workspacePathResolver.resolveToFile(new WorkspacePath(getBuildFilePath()));
+    File file = workspacePathResolver.resolveToFile(new WorkspacePath(buildFilePath));
     if (!file.exists()) {
       return BlazeValidationResult.failure("BUILD file does not exist.");
     }
+    BuildSystemProvider buildSystemProvider =
+        BuildSystemProvider.getBuildSystemProvider(builder.getBuildSystem());
+    checkState(buildSystemProvider != null);
+    if (!buildSystemProvider.isBuildFile(file.getName())) {
+      return BlazeValidationResult.failure("File must be a BUILD file.");
+    }
 
     return BlazeValidationResult.success();
   }
@@ -120,9 +136,9 @@
   }
 
   @Override
-  public String getDefaultProjectName(String workspaceName) {
+  public String getImportDirectory() {
     File buildFileParent = new File(getBuildFilePath()).getParentFile();
-    return buildFileParent != null ? buildFileParent.getName() : workspaceName;
+    return buildFileParent != null ? buildFileParent.getName() : null;
   }
 
   @Override
@@ -134,54 +150,31 @@
   private static String guessProjectViewFromLocation(
       WorkspacePathResolver workspacePathResolver, WorkspacePath workspacePath) {
 
-    WorkspacePath mainModuleWorkspaceRelativePath = workspacePath;
-    WorkspacePath testModuleWorkspaceRelativePath =
-        guessTestRelativePath(workspacePathResolver, mainModuleWorkspaceRelativePath);
+    List<WorkspacePath> workspacePaths = new ArrayList<>();
+    workspacePaths.add(workspacePath);
+    workspacePaths.addAll(
+        RelatedWorkspacePathFinder.getInstance()
+            .findRelatedWorkspaceDirectories(workspacePathResolver, workspacePath));
 
     ListSection.Builder<DirectoryEntry> directorySectionBuilder =
         ListSection.builder(DirectorySection.KEY);
-    directorySectionBuilder.add(DirectoryEntry.include(mainModuleWorkspaceRelativePath));
-    if (testModuleWorkspaceRelativePath != null) {
-      directorySectionBuilder.add(DirectoryEntry.include(testModuleWorkspaceRelativePath));
-    }
-
     ListSection.Builder<TargetExpression> targetSectionBuilder =
         ListSection.builder(TargetSection.KEY);
-    targetSectionBuilder.add(
-        TargetExpression.allFromPackageRecursive(mainModuleWorkspaceRelativePath));
-    if (testModuleWorkspaceRelativePath != null) {
-      targetSectionBuilder.add(
-          TargetExpression.allFromPackageRecursive(testModuleWorkspaceRelativePath));
-    }
+    workspacePaths.forEach(
+        path -> {
+          directorySectionBuilder.add(DirectoryEntry.include(path));
+          targetSectionBuilder.add(TargetExpression.allFromPackageRecursive(path));
+        });
 
     return ProjectViewParser.projectViewToString(
         ProjectView.builder()
             .add(directorySectionBuilder)
             .add(TextBlockSection.of(TextBlock.newLine()))
             .add(targetSectionBuilder)
+            .add(TextBlockSection.of(TextBlock.newLine()))
             .build());
   }
 
-  @Nullable
-  private static WorkspacePath guessTestRelativePath(
-      WorkspacePathResolver workspacePathResolver, WorkspacePath projectWorkspacePath) {
-    String projectRelativePath = projectWorkspacePath.relativePath();
-    String testBuildFileRelativePath = null;
-    if (projectRelativePath.startsWith("java/")) {
-      testBuildFileRelativePath = projectRelativePath.replaceFirst("java/", "javatests/");
-    } else if (projectRelativePath.contains("/java/")) {
-      testBuildFileRelativePath = projectRelativePath.replaceFirst("/java/", "/javatests/");
-    }
-    if (testBuildFileRelativePath != null) {
-      File testBuildFile =
-          workspacePathResolver.resolveToFile(new WorkspacePath(testBuildFileRelativePath));
-      if (testBuildFile.exists()) {
-        return new WorkspacePath(testBuildFileRelativePath);
-      }
-    }
-    return null;
-  }
-
   private String getBuildFilePath() {
     return buildFilePathField.getText().trim();
   }
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 fe43d1c..14a07e0 100644
--- a/base/src/com/google/idea/blaze/base/wizard2/ImportFromWorkspaceProjectViewOption.java
+++ b/base/src/com/google/idea/blaze/base/wizard2/ImportFromWorkspaceProjectViewOption.java
@@ -109,11 +109,11 @@
   }
 
   @Override
-  public String getDefaultProjectName(String workspaceName) {
+  public String getImportDirectory() {
     File projectViewFile = new File(getProjectViewPath());
     File projectViewDirectory = projectViewFile.getParentFile();
     if (projectViewDirectory == null) {
-      return workspaceName;
+      return null;
     }
     return projectViewDirectory.getName();
   }
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 51af079..de9d125 100644
--- a/base/src/com/google/idea/blaze/base/wizard2/UseExistingBazelWorkspaceOption.java
+++ b/base/src/com/google/idea/blaze/base/wizard2/UseExistingBazelWorkspaceOption.java
@@ -117,6 +117,11 @@
   }
 
   @Override
+  public String getBranchName() {
+    return null;
+  }
+
+  @Override
   public BlazeValidationResult validate() {
     if (getDirectory().isEmpty()) {
       return BlazeValidationResult.failure("Please select a workspace");
diff --git a/base/src/com/google/idea/blaze/base/wizard2/ui/BlazeEditProjectViewControl.java b/base/src/com/google/idea/blaze/base/wizard2/ui/BlazeEditProjectViewControl.java
index e936db3..5b652d8 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
@@ -15,6 +15,8 @@
  */
 package com.google.idea.blaze.base.wizard2.ui;
 
+import static java.util.stream.Collectors.toList;
+
 import com.google.common.collect.Lists;
 import com.google.common.hash.HashCode;
 import com.google.common.hash.Hashing;
@@ -26,15 +28,21 @@
 import com.google.idea.blaze.base.projectview.ProjectViewStorageManager;
 import com.google.idea.blaze.base.projectview.ProjectViewVerifier;
 import com.google.idea.blaze.base.projectview.parser.ProjectViewParser;
+import com.google.idea.blaze.base.projectview.section.ProjectViewDefaultValueProvider;
 import com.google.idea.blaze.base.projectview.section.ScalarSection;
+import com.google.idea.blaze.base.projectview.section.SectionKey;
 import com.google.idea.blaze.base.projectview.section.SectionParser;
+import com.google.idea.blaze.base.projectview.section.sections.DirectoryEntry;
+import com.google.idea.blaze.base.projectview.section.sections.DirectorySection;
 import com.google.idea.blaze.base.projectview.section.sections.ImportSection;
 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.scope.BlazeContext;
 import com.google.idea.blaze.base.scope.OutputSink.Propagation;
 import com.google.idea.blaze.base.scope.Scope;
 import com.google.idea.blaze.base.scope.output.IssueOutput;
 import com.google.idea.blaze.base.scope.output.IssueOutput.Category;
+import com.google.idea.blaze.base.settings.Blaze.BuildSystem;
 import com.google.idea.blaze.base.settings.ui.JPanelProvidingProject;
 import com.google.idea.blaze.base.settings.ui.ProjectViewUi;
 import com.google.idea.blaze.base.sync.BlazeSyncPlugin;
@@ -50,6 +58,7 @@
 import com.google.idea.blaze.base.wizard2.ProjectDataDirectoryValidator;
 import com.google.idea.common.experiments.BoolExperiment;
 import com.intellij.ide.RecentProjectsManager;
+import com.intellij.ide.util.PropertiesComponent;
 import com.intellij.openapi.Disposable;
 import com.intellij.openapi.application.ApplicationNamesInfo;
 import com.intellij.openapi.diagnostic.Logger;
@@ -66,13 +75,15 @@
 import java.awt.GridBagLayout;
 import java.io.File;
 import java.io.IOException;
+import java.util.Comparator;
 import java.util.List;
-import java.util.stream.Collectors;
+import javax.annotation.Nullable;
+import javax.swing.ButtonGroup;
 import javax.swing.JLabel;
 import javax.swing.JPanel;
+import javax.swing.JRadioButton;
 import javax.swing.JTextField;
 import org.jetbrains.annotations.NotNull;
-import org.jetbrains.annotations.Nullable;
 
 /** The UI control to collect project settings when importing a Blaze project. */
 public final class BlazeEditProjectViewControl {
@@ -83,6 +94,8 @@
 
   private static final BoolExperiment allowAddprojectViewDefaultValues =
       new BoolExperiment("allow.add.project.view.default.values", true);
+  private static final String LAST_WORKSPACE_MODE_PROPERTY =
+      "blaze.edit.project.view.control.last.workspace.mode";
 
   private final JPanel component;
   private final String buildSystemName;
@@ -90,14 +103,27 @@
 
   private TextFieldWithBrowseButton projectDataDirField;
   private JTextField projectNameField;
+  private JRadioButton workspaceDefaultNameOption;
+  private JRadioButton branchDefaultNameOption;
+  private JRadioButton importDirectoryDefaultNameOption;
   private HashCode paramsHash;
   private WorkspaceRoot workspaceRoot;
   private WorkspacePathResolver workspacePathResolver;
+  private BlazeSelectWorkspaceOption workspaceOption;
+  private BlazeSelectProjectViewOption projectViewOption;
+  private boolean isInitialising;
+  private boolean defaultWorkspaceNameModeExplicitlySet;
+
+  private enum InferDefaultNameMode {
+    FromWorkspace,
+    FromBranch,
+    FromImportDirectory,
+  }
 
   public BlazeEditProjectViewControl(BlazeNewProjectBuilder builder, Disposable parentDisposable) {
     this.projectViewUi = new ProjectViewUi(parentDisposable);
     JPanel component = new JPanelProvidingProject(ProjectViewUi.getProject(), new GridBagLayout());
-    fillUi(component, 0);
+    fillUi(component);
     update(builder);
     UiUtil.fillBottom(component);
     this.component = component;
@@ -108,12 +134,12 @@
     return component;
   }
 
-  private void fillUi(JPanel canvas, int indentLevel) {
+  private void fillUi(JPanel canvas) {
     JLabel projectDataDirLabel = new JBLabel("Project data directory:");
 
     Dimension minSize = ProjectViewUi.getMinimumSize();
-    // Add 120 pixels so we have room for our extra fields
-    minSize.setSize(minSize.width, minSize.height + 120);
+    // Add pixels so we have room for our extra fields
+    minSize.setSize(minSize.width, minSize.height + 180);
     canvas.setMinimumSize(minSize);
     canvas.setPreferredSize(minSize);
 
@@ -131,7 +157,7 @@
     projectDataDirField.setToolTipText(dataDirToolTipText);
     projectDataDirLabel.setToolTipText(dataDirToolTipText);
 
-    canvas.add(projectDataDirLabel, UiUtil.getLabelConstraints(indentLevel));
+    canvas.add(projectDataDirLabel, UiUtil.getLabelConstraints(0));
     canvas.add(projectDataDirField, UiUtil.getFillLineConstraints(0));
 
     JLabel projectNameLabel = new JLabel("Project name:");
@@ -139,17 +165,39 @@
     final String projectNameToolTipText = "Project display name.";
     projectNameField.setToolTipText(projectNameToolTipText);
     projectNameLabel.setToolTipText(projectNameToolTipText);
-    canvas.add(projectNameLabel, UiUtil.getLabelConstraints(indentLevel));
+    canvas.add(projectNameLabel, UiUtil.getLabelConstraints(0));
     canvas.add(projectNameField, UiUtil.getFillLineConstraints(0));
 
-    projectViewUi.fillUi(canvas, indentLevel);
+    JLabel defaultNameLabel = new JLabel("Infer name from:");
+    workspaceDefaultNameOption = new JRadioButton("Workspace");
+    branchDefaultNameOption = new JRadioButton("Branch");
+    importDirectoryDefaultNameOption = new JRadioButton("Import Directory");
+
+    workspaceDefaultNameOption.setToolTipText("Infer default name from the workspace name");
+    branchDefaultNameOption.setToolTipText(
+        "Infer default name from the current branch of your workspace");
+    importDirectoryDefaultNameOption.setToolTipText(
+        "Infer default name from the directory used to import your project view");
+
+    workspaceDefaultNameOption.addItemListener(e -> inferDefaultNameModeSelectionChanged());
+    branchDefaultNameOption.addItemListener(e -> inferDefaultNameModeSelectionChanged());
+    importDirectoryDefaultNameOption.addItemListener(e -> inferDefaultNameModeSelectionChanged());
+    ButtonGroup buttonGroup = new ButtonGroup();
+    buttonGroup.add(workspaceDefaultNameOption);
+    buttonGroup.add(branchDefaultNameOption);
+    buttonGroup.add(importDirectoryDefaultNameOption);
+    canvas.add(defaultNameLabel, UiUtil.getLabelConstraints(0));
+    canvas.add(workspaceDefaultNameOption, UiUtil.getLabelConstraints(0));
+    canvas.add(branchDefaultNameOption, UiUtil.getLabelConstraints(0));
+    canvas.add(importDirectoryDefaultNameOption, UiUtil.getLabelConstraints(0));
+    canvas.add(new JPanel(), UiUtil.getFillLineConstraints(0));
+
+    projectViewUi.fillUi(canvas);
   }
 
   public void update(BlazeNewProjectBuilder builder) {
-    BlazeSelectWorkspaceOption workspaceOption = builder.getWorkspaceOption();
-    BlazeSelectProjectViewOption projectViewOption = builder.getProjectViewOption();
-    String defaultProjectName =
-        projectViewOption.getDefaultProjectName(workspaceOption.getWorkspaceName());
+    this.workspaceOption = builder.getWorkspaceOption();
+    this.projectViewOption = builder.getProjectViewOption();
     WorkspaceRoot workspaceRoot = workspaceOption.getWorkspaceRoot();
     WorkspacePath workspacePath = projectViewOption.getSharedProjectView();
     String initialProjectViewText = projectViewOption.getInitialProjectViewText();
@@ -161,7 +209,6 @@
     HashCode hashCode =
         Hashing.md5()
             .newHasher()
-            .putUnencodedChars(defaultProjectName)
             .putUnencodedChars(workspaceRoot.toString())
             .putUnencodedChars(workspacePath != null ? workspacePath.toString() : "")
             .putUnencodedChars(initialProjectViewText != null ? initialProjectViewText : "")
@@ -171,18 +218,22 @@
     // If any params have changed, reinit the control
     if (!hashCode.equals(paramsHash)) {
       this.paramsHash = hashCode;
+      this.isInitialising = true;
       init(
-          defaultProjectName,
+          workspaceOption.getBuildSystemForWorkspace(),
           workspaceRoot,
           workspacePathResolver,
           workspacePath,
           initialProjectViewText,
           allowAddDefaultValues);
+      this.isInitialising = false;
     }
   }
 
   private static String modifyInitialProjectView(
-      String initialProjectViewText, WorkspacePathResolver workspacePathResolver) {
+      BuildSystem buildSystem,
+      String initialProjectViewText,
+      WorkspacePathResolver workspacePathResolver) {
     BlazeContext context = new BlazeContext();
     ProjectViewParser projectViewParser = new ProjectViewParser(context, workspacePathResolver);
     projectViewParser.parseProjectView(initialProjectViewText);
@@ -192,14 +243,23 @@
       return initialProjectViewText;
     }
     ProjectView projectView = projectViewFile.projectView;
-    for (SectionParser sectionParser : Sections.getParsers()) {
-      projectView = sectionParser.addProjectViewDefaultValue(projectView);
+
+    // Sort default value providers to match the section order
+    List<SectionKey> sectionKeys =
+        Sections.getParsers().stream().map(SectionParser::getSectionKey).collect(toList());
+    List<ProjectViewDefaultValueProvider> defaultValueProviders =
+        Lists.newArrayList(ProjectViewDefaultValueProvider.EP_NAME.getExtensions());
+    defaultValueProviders.sort(
+        Comparator.comparingInt(val -> sectionKeys.indexOf(val.getSectionKey())));
+    for (ProjectViewDefaultValueProvider defaultValueProvider : defaultValueProviders) {
+      projectView =
+          defaultValueProvider.addProjectViewDefaultValue(buildSystem, projectViewSet, projectView);
     }
     return ProjectViewParser.projectViewToString(projectView);
   }
 
   private void init(
-      String defaultProjectName,
+      BuildSystem buildSystem,
       WorkspaceRoot workspaceRoot,
       WorkspacePathResolver workspacePathResolver,
       @Nullable WorkspacePath sharedProjectView,
@@ -207,14 +267,13 @@
       boolean allowAddDefaultValues) {
     if (allowAddDefaultValues && initialProjectViewText != null) {
       initialProjectViewText =
-          modifyInitialProjectView(initialProjectViewText, workspacePathResolver);
+          modifyInitialProjectView(buildSystem, initialProjectViewText, workspacePathResolver);
     }
-
     this.workspaceRoot = workspaceRoot;
     this.workspacePathResolver = workspacePathResolver;
-    projectNameField.setText(defaultProjectName);
-    String defaultDataDir = getDefaultProjectDataDirectory(defaultProjectName);
-    projectDataDirField.setText(defaultDataDir);
+
+    updateDefaultProjectNameUiState();
+    updateDefaultProjectName();
 
     String projectViewText = "";
     File sharedProjectViewFile = null;
@@ -246,6 +305,82 @@
         false /* allowEditShared - not allowed during import */);
   }
 
+  private void updateDefaultProjectNameUiState() {
+    workspaceDefaultNameOption.setEnabled(true);
+    branchDefaultNameOption.setEnabled(workspaceOption.getBranchName() != null);
+    importDirectoryDefaultNameOption.setEnabled(projectViewOption.getImportDirectory() != null);
+
+    InferDefaultNameMode inferDefaultNameMode = InferDefaultNameMode.FromImportDirectory;
+    try {
+      String lastModeString =
+          PropertiesComponent.getInstance().getValue(LAST_WORKSPACE_MODE_PROPERTY);
+      if (lastModeString != null) {
+        inferDefaultNameMode = InferDefaultNameMode.valueOf(lastModeString);
+      }
+    } catch (IllegalArgumentException e) {
+      // Ignore
+    }
+    switch (inferDefaultNameMode) {
+      case FromWorkspace:
+        workspaceDefaultNameOption.setSelected(true);
+        break;
+      case FromBranch:
+        if (workspaceOption.getBranchName() != null) {
+          branchDefaultNameOption.setSelected(true);
+        } else {
+          workspaceDefaultNameOption.setSelected(true);
+        }
+        break;
+      case FromImportDirectory:
+        if (projectViewOption.getImportDirectory() != null) {
+          importDirectoryDefaultNameOption.setSelected(true);
+        } else {
+          workspaceDefaultNameOption.setSelected(true);
+        }
+        break;
+      default:
+        throw new AssertionError("Illegal workspace name mode");
+    }
+  }
+
+  private InferDefaultNameMode getInferDefaultNameMode() {
+    if (workspaceDefaultNameOption.isSelected()) {
+      return InferDefaultNameMode.FromWorkspace;
+    } else if (branchDefaultNameOption.isSelected()) {
+      return InferDefaultNameMode.FromBranch;
+    } else if (importDirectoryDefaultNameOption.isSelected()) {
+      return InferDefaultNameMode.FromImportDirectory;
+    }
+    return InferDefaultNameMode.FromWorkspace;
+  }
+
+  private void inferDefaultNameModeSelectionChanged() {
+    if (!isInitialising) {
+      updateDefaultProjectName();
+      this.defaultWorkspaceNameModeExplicitlySet = true;
+    }
+  }
+
+  private void updateDefaultProjectName() {
+    String defaultProjectName = getDefaultName(getInferDefaultNameMode());
+    projectNameField.setText(defaultProjectName);
+    String defaultDataDir = getDefaultProjectDataDirectory(defaultProjectName);
+    projectDataDirField.setText(defaultDataDir);
+  }
+
+  private String getDefaultName(InferDefaultNameMode inferDefaultNameMode) {
+    switch (inferDefaultNameMode) {
+      case FromWorkspace:
+        return workspaceOption.getWorkspaceName();
+      case FromBranch:
+        return workspaceOption.getBranchName();
+      case FromImportDirectory:
+        return projectViewOption.getImportDirectory();
+      default:
+        throw new AssertionError("Invalid workspace name mode.");
+    }
+  }
+
   private static String getDefaultProjectDataDirectory(String projectName) {
     File defaultDataDirectory = new File(getDefaultProjectsDirectory());
     File desiredLocation = new File(defaultDataDirectory, projectName);
@@ -347,6 +482,16 @@
               + "Please report a bug.");
     }
 
+    List<DirectoryEntry> directories = projectViewSet.listItems(DirectorySection.KEY);
+    if (directories.isEmpty()) {
+      String msg = "Add some directories to index in the 'directories' section.";
+      if (projectViewSet.listItems(TargetSection.KEY).isEmpty()) {
+        msg += "\nTargets are also generally required to resolve sources.";
+      }
+      return BlazeValidationResult.failure(msg);
+    }
+
+
     return BlazeValidationResult.success();
   }
 
@@ -382,12 +527,12 @@
         syncPlugin.installSdks(context);
       }
       WorkspaceLanguageSettings workspaceLanguageSettings =
-          LanguageSupport.createWorkspaceLanguageSettings(context, projectViewSet);
+          LanguageSupport.createWorkspaceLanguageSettings(projectViewSet);
       if (workspaceLanguageSettings == null) {
         return false;
       }
       return ProjectViewVerifier.verifyProjectView(
-          context, workspacePathResolver, projectViewSet, workspaceLanguageSettings);
+          null, context, workspacePathResolver, projectViewSet, workspaceLanguageSettings);
     }
   }
 
@@ -397,7 +542,7 @@
         issues
             .stream()
             .filter(issue -> issue.getCategory() == IssueOutput.Category.ERROR)
-            .collect(Collectors.toList());
+            .collect(toList());
 
     if (!errors.isEmpty()) {
       StringBuilder errorMessage = new StringBuilder();
@@ -452,4 +597,12 @@
         .setProjectName(projectName)
         .setProjectDataDirectory(projectDataDirectory);
   }
+
+  public void commit() {
+    if (defaultWorkspaceNameModeExplicitlySet) {
+      InferDefaultNameMode inferDefaultNameMode = getInferDefaultNameMode();
+      PropertiesComponent.getInstance()
+          .setValue(LAST_WORKSPACE_MODE_PROPERTY, inferDefaultNameMode.toString());
+    }
+  }
 }
diff --git a/base/tests/integrationtests/com/google/idea/blaze/base/lang/buildfile/actions/BuildFileModifierTest.java b/base/tests/integrationtests/com/google/idea/blaze/base/lang/buildfile/actions/BuildFileModifierTest.java
new file mode 100644
index 0000000..691dc74
--- /dev/null
+++ b/base/tests/integrationtests/com/google/idea/blaze/base/lang/buildfile/actions/BuildFileModifierTest.java
@@ -0,0 +1,46 @@
+/*
+ * Copyright 2017 The Bazel Authors. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.idea.blaze.base.lang.buildfile.actions;
+
+import com.google.idea.blaze.base.buildmodifier.BuildFileModifier;
+import com.google.idea.blaze.base.lang.buildfile.BuildFileIntegrationTestCase;
+import com.google.idea.blaze.base.lang.buildfile.psi.BuildFile;
+import com.google.idea.blaze.base.model.primitives.Kind;
+import com.google.idea.blaze.base.model.primitives.Label;
+import com.google.idea.blaze.base.model.primitives.WorkspacePath;
+import com.google.idea.blaze.base.scope.BlazeContext;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** Integration tests for {@link BuildFileModifier}. */
+@RunWith(JUnit4.class)
+public class BuildFileModifierTest extends BuildFileIntegrationTestCase {
+
+  @Test
+  public void testAddNewTarget() {
+    BuildFile buildFile =
+        createBuildFile(new WorkspacePath("BUILD"), "java_library(name = 'existing')", "");
+    BuildFileModifier.getInstance()
+        .addRule(getProject(), new BlazeContext(), Label.create("//:new_target"), Kind.JAVA_TEST);
+    assertFileContents(
+        buildFile,
+        "java_library(name = 'existing')",
+        "java_test(",
+        "    name = \"new_target\"",
+        ")");
+  }
+}
diff --git a/base/tests/integrationtests/com/google/idea/blaze/base/lang/buildfile/completion/FilePathCompletionTest.java b/base/tests/integrationtests/com/google/idea/blaze/base/lang/buildfile/completion/FilePathCompletionTest.java
index a13d261..a592d4e 100644
--- a/base/tests/integrationtests/com/google/idea/blaze/base/lang/buildfile/completion/FilePathCompletionTest.java
+++ b/base/tests/integrationtests/com/google/idea/blaze/base/lang/buildfile/completion/FilePathCompletionTest.java
@@ -20,6 +20,7 @@
 import com.google.idea.blaze.base.lang.buildfile.BuildFileIntegrationTestCase;
 import com.google.idea.blaze.base.lang.buildfile.psi.BuildFile;
 import com.google.idea.blaze.base.model.primitives.WorkspacePath;
+import com.intellij.codeInsight.CodeInsightSettings;
 import com.intellij.openapi.editor.Editor;
 import com.intellij.openapi.vfs.VirtualFile;
 import org.junit.Test;
@@ -44,6 +45,30 @@
   }
 
   @Test
+  public void testInsertPairQuoteOptionRespected() {
+    boolean old = CodeInsightSettings.getInstance().AUTOINSERT_PAIR_QUOTE;
+    try {
+      CodeInsightSettings.getInstance().AUTOINSERT_PAIR_QUOTE = false;
+      BuildFile file = createBuildFile(new WorkspacePath("java/BUILD"), "'//");
+      Editor editor = editorTest.openFileInEditor(file);
+      editorTest.setCaretPosition(editor, 0, "'//".length());
+
+      assertThat(editorTest.completeIfUnique()).isTrue();
+      assertFileContents(file, "'//java");
+
+      CodeInsightSettings.getInstance().AUTOINSERT_PAIR_QUOTE = true;
+      file = createBuildFile(new WorkspacePath("foo/BUILD"), "'//j");
+      editor = editorTest.openFileInEditor(file);
+      editorTest.setCaretPosition(editor, 0, "'//j".length());
+
+      assertThat(editorTest.completeIfUnique()).isTrue();
+      assertFileContents(file, "'//java'");
+    } finally {
+      CodeInsightSettings.getInstance().AUTOINSERT_PAIR_QUOTE = old;
+    }
+  }
+
+  @Test
   public void testUniqueMultiSegmentDirectoryCompleted() {
     BuildFile file = createBuildFile(new WorkspacePath("java/com/google/BUILD"), "'//'");
 
diff --git a/base/tests/integrationtests/com/google/idea/blaze/base/lang/buildfile/completion/RuleTargetCompletionTest.java b/base/tests/integrationtests/com/google/idea/blaze/base/lang/buildfile/completion/RuleTargetCompletionTest.java
index 980da4d..ce2388b 100644
--- a/base/tests/integrationtests/com/google/idea/blaze/base/lang/buildfile/completion/RuleTargetCompletionTest.java
+++ b/base/tests/integrationtests/com/google/idea/blaze/base/lang/buildfile/completion/RuleTargetCompletionTest.java
@@ -17,11 +17,17 @@
 
 import static com.google.common.truth.Truth.assertThat;
 
+import com.google.common.collect.ImmutableMap;
 import com.google.idea.blaze.base.lang.buildfile.BuildFileIntegrationTestCase;
+import com.google.idea.blaze.base.lang.buildfile.language.semantics.BuildLanguageSpec;
+import com.google.idea.blaze.base.lang.buildfile.language.semantics.BuildLanguageSpecProvider;
+import com.google.idea.blaze.base.lang.buildfile.language.semantics.RuleDefinition;
 import com.google.idea.blaze.base.lang.buildfile.psi.BuildFile;
 import com.google.idea.blaze.base.model.primitives.WorkspacePath;
 import com.intellij.codeInsight.lookup.LookupElement;
 import com.intellij.openapi.editor.Editor;
+import com.intellij.openapi.project.Project;
+import javax.annotation.Nullable;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.junit.runners.JUnit4;
@@ -49,6 +55,28 @@
   }
 
   @Test
+  public void testCustomRuleCompletion() {
+    MockBuildLanguageSpecProvider specProvider = new MockBuildLanguageSpecProvider();
+    setBuildLanguageSpecRules(specProvider, "java_library");
+    registerApplicationService(BuildLanguageSpecProvider.class, specProvider);
+
+    BuildFile file =
+        createBuildFile(
+            new WorkspacePath("java/com/google/BUILD"),
+            "custom_rule(name = 'lib')",
+            "java_library(",
+            "    name = 'test',",
+            "    deps = [':']");
+
+    Editor editor = editorTest.openFileInEditor(file);
+    editorTest.setCaretPosition(editor, 3, "    deps = [':".length());
+
+    LookupElement[] completionItems = testFixture.completeBasic();
+    assertThat(completionItems).hasLength(1);
+    assertThat(completionItems[0].toString()).isEqualTo("':lib'");
+  }
+
+  @Test
   public void testIgnoreContainingTarget() {
     BuildFile file =
         createBuildFile(
@@ -160,4 +188,28 @@
     assertThat(completionItems).asList().contains("'//java:root_rule'");
     assertThat(completionItems).asList().doesNotContain("'//java/com/google:other_rule'");
   }
+
+  private static void setBuildLanguageSpecRules(
+      MockBuildLanguageSpecProvider specProvider, String... ruleNames) {
+    ImmutableMap.Builder<String, RuleDefinition> rules = ImmutableMap.builder();
+    for (String name : ruleNames) {
+      rules.put(name, new RuleDefinition(name, ImmutableMap.of(), null));
+    }
+    specProvider.setRules(rules.build());
+  }
+
+  private static class MockBuildLanguageSpecProvider implements BuildLanguageSpecProvider {
+
+    BuildLanguageSpec languageSpec = new BuildLanguageSpec(ImmutableMap.of());
+
+    void setRules(ImmutableMap<String, RuleDefinition> rules) {
+      languageSpec = new BuildLanguageSpec(rules);
+    }
+
+    @Nullable
+    @Override
+    public BuildLanguageSpec getLanguageSpec(Project project) {
+      return languageSpec;
+    }
+  }
 }
diff --git a/base/tests/integrationtests/com/google/idea/blaze/base/lang/buildfile/completion/SkylarkExtensionSymbolCompletionTest.java b/base/tests/integrationtests/com/google/idea/blaze/base/lang/buildfile/completion/SkylarkExtensionSymbolCompletionTest.java
index 44f3cb9..7a297ac 100644
--- a/base/tests/integrationtests/com/google/idea/blaze/base/lang/buildfile/completion/SkylarkExtensionSymbolCompletionTest.java
+++ b/base/tests/integrationtests/com/google/idea/blaze/base/lang/buildfile/completion/SkylarkExtensionSymbolCompletionTest.java
@@ -96,4 +96,18 @@
     assertThat(editorTest.completeIfUnique()).isTrue();
     assertFileContents(file, "load(':skylark.bzl', 'function')");
   }
+
+  @Test
+  public void testPrivateSymbolsNotAutocompleted() {
+    workspace.createFile(
+        new WorkspacePath("skylark.bzl"),
+        "_local = 1",
+        "GLOBAL_VAR = 2",
+        "def _local_fn():stmt",
+        "def global_fn():stmt");
+    createAndSetCaret(new WorkspacePath("BUILD"), "load(':skylark.bzl', '<caret>')");
+
+    String[] options = editorTest.getCompletionItemsAsStrings();
+    assertThat(options).asList().containsExactly("'GLOBAL_VAR'", "'global_fn'");
+  }
 }
diff --git a/base/tests/integrationtests/com/google/idea/blaze/base/lang/buildfile/findusages/ExternalFileUsagesTest.java b/base/tests/integrationtests/com/google/idea/blaze/base/lang/buildfile/findusages/ExternalFileUsagesTest.java
index f50dbf2..9964a22 100644
--- a/base/tests/integrationtests/com/google/idea/blaze/base/lang/buildfile/findusages/ExternalFileUsagesTest.java
+++ b/base/tests/integrationtests/com/google/idea/blaze/base/lang/buildfile/findusages/ExternalFileUsagesTest.java
@@ -61,7 +61,7 @@
 
     PsiElement ref = references[0].getElement();
     assertThat(ref).isInstanceOf(StringLiteral.class);
-    assertThat(PsiUtils.getParentOfType(ref, Argument.Keyword.class)).isEqualTo(arg);
+    assertThat(PsiUtils.getParentOfType(ref, Argument.Keyword.class, true)).isEqualTo(arg);
   }
 
   @Test
diff --git a/base/tests/integrationtests/com/google/idea/blaze/base/lang/buildfile/findusages/ExternalWorkspaceFindUsagesTest.java b/base/tests/integrationtests/com/google/idea/blaze/base/lang/buildfile/findusages/ExternalWorkspaceFindUsagesTest.java
new file mode 100644
index 0000000..aec776f
--- /dev/null
+++ b/base/tests/integrationtests/com/google/idea/blaze/base/lang/buildfile/findusages/ExternalWorkspaceFindUsagesTest.java
@@ -0,0 +1,124 @@
+/*
+ * Copyright 2017 The Bazel Authors. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.idea.blaze.base.lang.buildfile.findusages;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.google.idea.blaze.base.lang.buildfile.BuildFileIntegrationTestCase;
+import com.google.idea.blaze.base.lang.buildfile.psi.Argument;
+import com.google.idea.blaze.base.lang.buildfile.psi.BuildFile;
+import com.google.idea.blaze.base.lang.buildfile.psi.FuncallExpression;
+import com.google.idea.blaze.base.lang.buildfile.psi.StringLiteral;
+import com.google.idea.blaze.base.lang.buildfile.psi.util.PsiUtils;
+import com.google.idea.blaze.base.lang.buildfile.search.FindUsages;
+import com.google.idea.blaze.base.model.primitives.WorkspacePath;
+import com.google.idea.blaze.base.settings.Blaze.BuildSystem;
+import com.google.idea.blaze.base.sync.data.BlazeProjectDataManager;
+import com.google.idea.blaze.base.sync.workspace.WorkspaceHelper;
+import com.intellij.psi.PsiFile;
+import com.intellij.psi.PsiReference;
+import java.io.File;
+import java.nio.file.Paths;
+import java.util.Arrays;
+import java.util.stream.Collectors;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** Test that references to external workspaces appear in 'find usages' results. */
+@RunWith(JUnit4.class)
+public class ExternalWorkspaceFindUsagesTest extends BuildFileIntegrationTestCase {
+
+  @Override
+  protected BuildSystem buildSystem() {
+    return BuildSystem.Bazel;
+  }
+
+  @Test
+  public void testFindUsagesFromWorkspaceFile() {
+    BuildFile workspaceBuildFile =
+        createBuildFile(
+            new WorkspacePath("WORKSPACE"),
+            "maven_jar(",
+            "    name = 'javax_inject',",
+            "    artifact = 'javax.inject:javax.inject:1',",
+            "    sha1 = '6975da39a7040257bd51d21a231b76c915872d38',",
+            ")");
+    BuildFile refFile1 =
+        createBuildFile(
+            new WorkspacePath("java/com/foo/BUILD"),
+            "java_library(name = 'javax_inject', exports = ['@javax_inject//jar'])");
+
+    BuildFile refFile2 =
+        createBuildFile(
+            new WorkspacePath("java/com/bar/build_defs.bzl"),
+            "DEP = '@javax_inject//invalid:nonsense'");
+
+    FuncallExpression target = workspaceBuildFile.findRule("javax_inject");
+    StringLiteral ref1 =
+        PsiUtils.findFirstChildOfClassRecursive(
+            refFile1.findRule("javax_inject").getKeywordArgument("exports"), StringLiteral.class);
+    StringLiteral ref2 = PsiUtils.findFirstChildOfClassRecursive(refFile2, StringLiteral.class);
+
+    PsiReference[] references = FindUsages.findAllReferences(target);
+    assertThat(references).hasLength(2);
+    assertThat(Arrays.stream(references).map(PsiReference::getElement).collect(Collectors.toList()))
+        .containsExactly(ref1, ref2);
+  }
+
+  @Test
+  public void testFindUsagesFromExternalWorkspaceFile() {
+    BuildFile workspaceBuildFile =
+        createBuildFile(
+            new WorkspacePath("BUILD"),
+            "java_library(",
+            "    name = 'lib',",
+            "    exports = ['@junit//:jar'],",
+            ")");
+    BuildFile externalFile =
+        (BuildFile)
+            createFileInExternalWorkspace(
+                "junit",
+                new WorkspacePath("BUILD"),
+                "java_import(",
+                "    name = 'jar',",
+                "    jars = ['junit-4.11.jar'],",
+                ")");
+
+    FuncallExpression target = externalFile.findRule("jar");
+    assertThat(target).isNotNull();
+
+    Argument.Keyword arg = workspaceBuildFile.findRule("lib").getKeywordArgument("exports");
+    StringLiteral label = PsiUtils.findFirstChildOfClassRecursive(arg, StringLiteral.class);
+    assertThat(label).isNotNull();
+
+    PsiReference[] references = FindUsages.findAllReferences(target);
+    assertThat(references).hasLength(1);
+    assertThat(references[0].getElement()).isEqualTo(label);
+  }
+
+  private PsiFile createFileInExternalWorkspace(
+      String workspaceName, WorkspacePath path, String... contents) {
+    String filePath =
+        Paths.get(getExternalSourceRoot().getPath(), workspaceName, path.relativePath()).toString();
+    return fileSystem.createPsiFile(filePath, contents);
+  }
+
+  private File getExternalSourceRoot() {
+    return WorkspaceHelper.getExternalSourceRoot(
+        BlazeProjectDataManager.getInstance(getProject()).getBlazeProjectData());
+  }
+}
diff --git a/base/tests/integrationtests/com/google/idea/blaze/base/lang/buildfile/findusages/LocalVariableUsagesTest.java b/base/tests/integrationtests/com/google/idea/blaze/base/lang/buildfile/findusages/LocalVariableUsagesTest.java
index 220c8f2..d46c259 100644
--- a/base/tests/integrationtests/com/google/idea/blaze/base/lang/buildfile/findusages/LocalVariableUsagesTest.java
+++ b/base/tests/integrationtests/com/google/idea/blaze/base/lang/buildfile/findusages/LocalVariableUsagesTest.java
@@ -61,14 +61,16 @@
     assertThat(funcall).isNotNull();
 
     PsiElement firstRef = references[0].getElement();
-    assertThat(PsiUtils.getParentOfType(firstRef, FuncallExpression.class)).isEqualTo(funcall);
+    assertThat(PsiUtils.getParentOfType(firstRef, FuncallExpression.class, true))
+        .isEqualTo(funcall);
 
     FunctionStatement function = buildFile.findChildByClass(FunctionStatement.class);
     assertThat(function).isNotNull();
 
     PsiElement secondRef = references[1].getElement();
     assertThat(secondRef.getParent()).isInstanceOf(AssignmentStatement.class);
-    assertThat(PsiUtils.getParentOfType(secondRef, FunctionStatement.class)).isEqualTo(function);
+    assertThat(PsiUtils.getParentOfType(secondRef, FunctionStatement.class, true))
+        .isEqualTo(function);
   }
 
   // the case where a symbol is the target of multiple assignment statements
diff --git a/base/tests/integrationtests/com/google/idea/blaze/base/lang/buildfile/quickfix/DeprecatedLoadQuickFixTest.java b/base/tests/integrationtests/com/google/idea/blaze/base/lang/buildfile/quickfix/DeprecatedLoadQuickFixTest.java
new file mode 100644
index 0000000..4223a4d
--- /dev/null
+++ b/base/tests/integrationtests/com/google/idea/blaze/base/lang/buildfile/quickfix/DeprecatedLoadQuickFixTest.java
@@ -0,0 +1,104 @@
+/*
+ * Copyright 2017 The Bazel Authors. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.idea.blaze.base.lang.buildfile.quickfix;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.google.idea.blaze.base.lang.buildfile.BuildFileIntegrationTestCase;
+import com.google.idea.blaze.base.lang.buildfile.psi.BuildFile;
+import com.google.idea.blaze.base.lang.buildfile.psi.StringLiteral;
+import com.google.idea.blaze.base.lang.buildfile.psi.util.PsiUtils;
+import com.google.idea.blaze.base.model.primitives.WorkspacePath;
+import com.intellij.codeInspection.ProblemDescriptor;
+import com.intellij.codeInspection.ProblemHighlightType;
+import com.intellij.openapi.command.WriteCommandAction;
+import com.intellij.psi.PsiElement;
+import com.intellij.testFramework.MockProblemDescriptor;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** Integration tests for {@link DeprecatedLoadQuickFix}. */
+@RunWith(JUnit4.class)
+public class DeprecatedLoadQuickFixTest extends BuildFileIntegrationTestCase {
+
+  @Test
+  public void testParentDirectoryHasNoBuildFile() {
+    BuildFile file =
+        createBuildFile(
+            new WorkspacePath("java/com/google/BUILD"),
+            "load('/java/com/google/subdir/build_defs', 'symbol')");
+
+    StringLiteral string = PsiUtils.findFirstChildOfClassRecursive(file, StringLiteral.class);
+    applyQuickFix(string);
+
+    assertThat(string.getStringContents()).isEqualTo("//java/com/google:subdir/build_defs.bzl");
+  }
+
+  @Test
+  public void testBlazePackageIsParentDirectory() {
+    workspace.createPsiFile(new WorkspacePath("foo/bar/BUILD"));
+    workspace.createPsiFile(new WorkspacePath("foo/bar/build_defs.bzl"));
+
+    BuildFile file =
+        createBuildFile(
+            new WorkspacePath("java/com/google/BUILD"), "load('/foo/bar/build_defs', 'symbol')");
+
+    StringLiteral string = PsiUtils.findFirstChildOfClassRecursive(file, StringLiteral.class);
+    applyQuickFix(string);
+
+    assertThat(string.getStringContents()).isEqualTo("//foo/bar:build_defs.bzl");
+  }
+
+  @Test
+  public void testNormalLoadStatementUntouched() {
+    workspace.createPsiFile(new WorkspacePath("foo/bar/BUILD"));
+    workspace.createPsiFile(new WorkspacePath("foo/bar/build_defs.bzl"));
+
+    BuildFile file =
+        createBuildFile(
+            new WorkspacePath("java/com/google/BUILD"),
+            "load('//foo/bar:build_defs.bzl', 'symbol')");
+
+    StringLiteral string = PsiUtils.findFirstChildOfClassRecursive(file, StringLiteral.class);
+    String prevString = string.getStringContents();
+    applyQuickFix(string);
+    assertThat(string.getStringContents()).isEqualTo(prevString);
+  }
+
+  @Test
+  public void testRelativeLoadStatementUntouched() {
+    workspace.createPsiFile(new WorkspacePath("foo/bar/build_defs.bzl"));
+    BuildFile file =
+        createBuildFile(new WorkspacePath("foo/bar/BUILD"), "load(':build_defs.bzl', 'symbol')");
+
+    StringLiteral string = PsiUtils.findFirstChildOfClassRecursive(file, StringLiteral.class);
+    String prevString = string.getStringContents();
+    applyQuickFix(string);
+    assertThat(string.getStringContents()).isEqualTo(prevString);
+  }
+
+  private void applyQuickFix(StringLiteral string) {
+    WriteCommandAction.runWriteCommandAction(
+        getProject(),
+        () -> DeprecatedLoadQuickFix.INSTANCE.applyFix(getProject(), descriptorForPsi(string)));
+  }
+
+  private static ProblemDescriptor descriptorForPsi(PsiElement psi) {
+    return new MockProblemDescriptor(
+        psi, "mock", ProblemHighlightType.LIKE_DEPRECATED, DeprecatedLoadQuickFix.INSTANCE);
+  }
+}
diff --git a/base/tests/integrationtests/com/google/idea/blaze/base/lang/buildfile/refactor/RenameRefactoringTest.java b/base/tests/integrationtests/com/google/idea/blaze/base/lang/buildfile/refactor/RenameRefactoringTest.java
index 71f8962..7db4d29 100644
--- a/base/tests/integrationtests/com/google/idea/blaze/base/lang/buildfile/refactor/RenameRefactoringTest.java
+++ b/base/tests/integrationtests/com/google/idea/blaze/base/lang/buildfile/refactor/RenameRefactoringTest.java
@@ -36,6 +36,7 @@
 import com.intellij.refactoring.move.moveClassesOrPackages.MoveDirectoryWithClassesProcessor;
 import com.intellij.refactoring.rename.RenameDialog;
 import com.intellij.refactoring.rename.RenamePsiElementProcessor;
+import com.intellij.refactoring.rename.RenameUtil;
 import java.io.IOException;
 import java.util.Arrays;
 import java.util.List;
@@ -115,6 +116,32 @@
   }
 
   @Test
+  public void testTargetRenameValidation() {
+    BuildFile file =
+        createBuildFile(new WorkspacePath("com/google/foo/BUILD"), "rule_type(name = \"target\")");
+    FuncallExpression target =
+        PsiUtils.findFirstChildOfClassRecursive(file, FuncallExpression.class);
+
+    assertThat(RenameUtil.isValidName(getProject(), target, "name-with_allowed,meta=chars+-./@~"))
+        .isTrue();
+    assertThat(RenameUtil.isValidName(getProject(), target, "name:withColon")).isFalse();
+    assertThat(RenameUtil.isValidName(getProject(), target, "/start-with-slash")).isFalse();
+    assertThat(RenameUtil.isValidName(getProject(), target, "up-level-ref/../etc")).isFalse();
+  }
+
+  @Test
+  public void testFunctionRenameValidation() {
+    BuildFile file =
+        createBuildFile(new WorkspacePath("com/google/foo/BUILD"), "def fn_name():", "  return");
+    FunctionStatement fn = PsiUtils.findFirstChildOfClassRecursive(file, FunctionStatement.class);
+
+    assertThat(RenameUtil.isValidName(getProject(), fn, "name-with-dash")).isFalse();
+    assertThat(RenameUtil.isValidName(getProject(), fn, "name:withColon")).isFalse();
+    assertThat(RenameUtil.isValidName(getProject(), fn, "return")).isFalse();
+    assertThat(RenameUtil.isValidName(getProject(), fn, "name_with_underscore")).isTrue();
+  }
+
+  @Test
   public void testRenameSkylarkExtension() {
     BuildFile extFile =
         createBuildFile(
@@ -237,6 +264,37 @@
   }
 
   @Test
+  public void testRenameExternalWorkspaceTarget() {
+    BuildFile workspaceFile =
+        createBuildFile(
+            new WorkspacePath("WORKSPACE"),
+            "maven_jar(",
+            "    name = \"javax\",",
+            "    artifact = \"javax.inject:javax.inject:1\",",
+            "    sha1 = \"6975da39a7040257bd51d21a231b76c915872d38\",",
+            ")");
+    BuildFile referencingFile =
+        createBuildFile(
+            new WorkspacePath("java/com/foo/BUILD"),
+            "java_library(name = \"javax_inject\", exports = [\"@javax//jar\"])");
+
+    FuncallExpression targetRule =
+        PsiUtils.findFirstChildOfClassRecursive(workspaceFile, FuncallExpression.class);
+    testFixture.renameElement(targetRule, "v2_lib");
+
+    assertFileContents(
+        workspaceFile,
+        "maven_jar(",
+        "    name = \"v2_lib\",",
+        "    artifact = \"javax.inject:javax.inject:1\",",
+        "    sha1 = \"6975da39a7040257bd51d21a231b76c915872d38\",",
+        ")");
+
+    assertFileContents(
+        referencingFile, "java_library(name = \"javax_inject\", exports = [\"@v2_lib//jar\"])");
+  }
+
+  @Test
   public void testRenameSuggestionForBuildFile() {
     BuildFile buildFile = createBuildFile(new WorkspacePath("java/com/google/BUILD"));
     RenamePsiElementProcessor processor = RenamePsiElementProcessor.forElement(buildFile);
diff --git a/base/tests/integrationtests/com/google/idea/blaze/base/lang/buildfile/references/ExternalWorkspaceReferenceTest.java b/base/tests/integrationtests/com/google/idea/blaze/base/lang/buildfile/references/ExternalWorkspaceReferenceTest.java
new file mode 100644
index 0000000..6e2a045
--- /dev/null
+++ b/base/tests/integrationtests/com/google/idea/blaze/base/lang/buildfile/references/ExternalWorkspaceReferenceTest.java
@@ -0,0 +1,153 @@
+/*
+ * Copyright 2017 The Bazel Authors. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.idea.blaze.base.lang.buildfile.references;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.google.idea.blaze.base.lang.buildfile.BuildFileIntegrationTestCase;
+import com.google.idea.blaze.base.lang.buildfile.psi.Argument;
+import com.google.idea.blaze.base.lang.buildfile.psi.BuildFile;
+import com.google.idea.blaze.base.lang.buildfile.psi.FuncallExpression;
+import com.google.idea.blaze.base.lang.buildfile.psi.StringLiteral;
+import com.google.idea.blaze.base.lang.buildfile.psi.util.PsiUtils;
+import com.google.idea.blaze.base.model.primitives.WorkspacePath;
+import com.google.idea.blaze.base.settings.Blaze.BuildSystem;
+import com.google.idea.blaze.base.sync.data.BlazeProjectDataManager;
+import com.google.idea.blaze.base.sync.workspace.WorkspaceHelper;
+import com.intellij.psi.PsiFile;
+import java.io.File;
+import java.nio.file.Paths;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** Tests that labels referencing external workspaces are correctly resolved. */
+@RunWith(JUnit4.class)
+public class ExternalWorkspaceReferenceTest extends BuildFileIntegrationTestCase {
+
+  @Override
+  protected BuildSystem buildSystem() {
+    return BuildSystem.Bazel;
+  }
+
+  @Test
+  public void testExternalWorkspaceTargetReference() {
+    BuildFile workspaceBuildFile =
+        createBuildFile(
+            new WorkspacePath("BUILD"),
+            "java_library(",
+            "    name = 'lib',",
+            "    exports = ['@junit//:jar'],",
+            ")");
+    BuildFile externalBuildFile =
+        (BuildFile)
+            createFileInExternalWorkspace(
+                "junit",
+                new WorkspacePath("BUILD"),
+                "java_import(",
+                "    name = 'jar',",
+                "    jars = ['junit-4.11.jar'],",
+                ")");
+
+    FuncallExpression target = externalBuildFile.findRule("jar");
+    assertThat(target).isNotNull();
+
+    Argument.Keyword arg = workspaceBuildFile.findRule("lib").getKeywordArgument("exports");
+    StringLiteral label = PsiUtils.findFirstChildOfClassRecursive(arg, StringLiteral.class);
+    assertThat(label.getReferencedElement()).isEqualTo(target);
+  }
+
+  @Test
+  public void testLocalTargetReferenceWithinExternalWorkspaceResolves() {
+    BuildFile externalFile =
+        (BuildFile)
+            createFileInExternalWorkspace(
+                "junit",
+                new WorkspacePath("BUILD"),
+                "java_import(",
+                "    name = 'jar',",
+                "    jars = ['junit-4.11.jar'],",
+                ")",
+                "java_library(",
+                "    name = 'lib',",
+                "    srcs = [':jar'],",
+                ")");
+    FuncallExpression target = externalFile.findRule("jar");
+    assertThat(target).isNotNull();
+
+    Argument.Keyword arg = externalFile.findRule("lib").getKeywordArgument("srcs");
+    StringLiteral label = PsiUtils.findFirstChildOfClassRecursive(arg, StringLiteral.class);
+    assertThat(label.getReferencedElement()).isEqualTo(target);
+  }
+
+  @Test
+  public void testFileReferenceWithinExternalWorkspaceResolves() {
+    BuildFile externalFile =
+        (BuildFile)
+            createFileInExternalWorkspace(
+                "junit",
+                new WorkspacePath("BUILD"),
+                "java_import(",
+                "    name = 'target',",
+                "    jars = ['junit-4.11.jar'],",
+                ")");
+    PsiFile jarFile = createFileInExternalWorkspace("junit", new WorkspacePath("junit-4.11.jar"));
+    FuncallExpression target = externalFile.findRule("target");
+    StringLiteral label =
+        PsiUtils.findFirstChildOfClassRecursive(
+            target.getKeywordArgument("jars"), StringLiteral.class);
+    assertThat(label.getReferencedElement()).isEqualTo(jarFile);
+  }
+
+  @Test
+  public void testReferenceToWorkspaceFileContents() {
+    BuildFile workspaceFile =
+        createBuildFile(
+            new WorkspacePath("WORKSPACE"),
+            "maven_jar(",
+            "    name = 'w3c_css_sac',",
+            "    artifact = 'org.w3c.css:sac:1.3',",
+            "    sha1 = 'cdb2dcb4e22b83d6b32b93095f644c3462739e82',",
+            ")");
+    BuildFile referencingFile =
+        createBuildFile(
+            new WorkspacePath("java/com/google/pkg/BUILD"),
+            "rule(",
+            "    name = 'other',",
+            "    dep = '@w3c_css_sac//jar'",
+            ")");
+    FuncallExpression target = workspaceFile.findRule("w3c_css_sac");
+    assertThat(target).isNotNull();
+
+    FuncallExpression other = referencingFile.findRule("other");
+    StringLiteral label =
+        PsiUtils.findFirstChildOfClassRecursive(
+            other.getKeywordArgument("dep"), StringLiteral.class);
+    assertThat(label.getReferencedElement()).isEqualTo(target);
+  }
+
+  private PsiFile createFileInExternalWorkspace(
+      String workspaceName, WorkspacePath path, String... contents) {
+    String filePath =
+        Paths.get(getExternalSourceRoot().getPath(), workspaceName, path.relativePath()).toString();
+    return fileSystem.createPsiFile(filePath, contents);
+  }
+
+  private File getExternalSourceRoot() {
+    return WorkspaceHelper.getExternalSourceRoot(
+        BlazeProjectDataManager.getInstance(getProject()).getBlazeProjectData());
+  }
+}
diff --git a/base/tests/integrationtests/com/google/idea/blaze/base/lang/buildfile/references/GlobReferenceTest.java b/base/tests/integrationtests/com/google/idea/blaze/base/lang/buildfile/references/GlobReferenceTest.java
index cc9f04c..303ab02 100644
--- a/base/tests/integrationtests/com/google/idea/blaze/base/lang/buildfile/references/GlobReferenceTest.java
+++ b/base/tests/integrationtests/com/google/idea/blaze/base/lang/buildfile/references/GlobReferenceTest.java
@@ -137,8 +137,7 @@
     PsiFile foo = workspace.createPsiFile(new WorkspacePath("java/com/google/Foo.java"));
     BuildFile file =
         createBuildFile(
-            new WorkspacePath("java/com/google/BUILD"),
-            "glob(" + "  ['**/*']," + "  exclude = ['BUILD'])");
+            new WorkspacePath("java/com/google/BUILD"), "glob(['**/*'],  exclude = ['BUILD'])");
 
     GlobExpression glob = PsiUtils.findFirstChildOfClassRecursive(file, GlobExpression.class);
     List<PsiElement> references = multiResolve(glob);
@@ -147,6 +146,20 @@
   }
 
   @Test
+  public void testExcludeDirectories2() {
+    workspace.createDirectory(new WorkspacePath("java/com/google/tests"));
+    workspace.createPsiFile(new WorkspacePath("java/com/google/tests/Test.java"));
+    workspace.createPsiFile(new WorkspacePath("java/com/google/Foo.java"));
+    BuildFile file =
+        createBuildFile(
+            new WorkspacePath("java/com/google/BUILD"), "glob(['**/*'],  exclude = ['**/*'])");
+
+    GlobExpression glob = PsiUtils.findFirstChildOfClassRecursive(file, GlobExpression.class);
+    List<PsiElement> references = multiResolve(glob);
+    assertThat(references).isEmpty();
+  }
+
+  @Test
   public void testFilesInSubpackagesExcluded() {
     BuildFile pkg =
         createBuildFile(new WorkspacePath("java/com/google/BUILD"), "glob(['**/*.java'])");
diff --git a/base/tests/integrationtests/com/google/idea/blaze/base/lang/buildfile/references/LabelReferenceTest.java b/base/tests/integrationtests/com/google/idea/blaze/base/lang/buildfile/references/LabelReferenceTest.java
index b14f4b7..7eb16c0 100644
--- a/base/tests/integrationtests/com/google/idea/blaze/base/lang/buildfile/references/LabelReferenceTest.java
+++ b/base/tests/integrationtests/com/google/idea/blaze/base/lang/buildfile/references/LabelReferenceTest.java
@@ -177,7 +177,7 @@
     assertThat(references).hasLength(1);
 
     PsiElement element = references[0].getElement();
-    FuncallExpression rule = PsiUtils.getParentOfType(element, FuncallExpression.class);
+    FuncallExpression rule = PsiUtils.getParentOfType(element, FuncallExpression.class, true);
     assertThat(rule.getName()).isEqualTo("bar");
     assertThat(rule.getContainingFile()).isEqualTo(referencingFile);
   }
diff --git a/base/tests/integrationtests/com/google/idea/blaze/base/lang/buildfile/references/LoadedSkylarkExtensionTest.java b/base/tests/integrationtests/com/google/idea/blaze/base/lang/buildfile/references/LoadedSkylarkExtensionTest.java
index b43f778..5cf33ab 100644
--- a/base/tests/integrationtests/com/google/idea/blaze/base/lang/buildfile/references/LoadedSkylarkExtensionTest.java
+++ b/base/tests/integrationtests/com/google/idea/blaze/base/lang/buildfile/references/LoadedSkylarkExtensionTest.java
@@ -17,14 +17,26 @@
 
 import static com.google.common.truth.Truth.assertThat;
 
+import com.google.common.collect.ImmutableMap;
 import com.google.idea.blaze.base.lang.buildfile.BuildFileIntegrationTestCase;
+import com.google.idea.blaze.base.lang.buildfile.language.semantics.BuildLanguageSpec;
+import com.google.idea.blaze.base.lang.buildfile.language.semantics.BuildLanguageSpecProvider;
+import com.google.idea.blaze.base.lang.buildfile.language.semantics.RuleDefinition;
 import com.google.idea.blaze.base.lang.buildfile.psi.BuildFile;
 import com.google.idea.blaze.base.lang.buildfile.psi.FuncallExpression;
 import com.google.idea.blaze.base.lang.buildfile.psi.FunctionStatement;
 import com.google.idea.blaze.base.lang.buildfile.psi.LoadStatement;
 import com.google.idea.blaze.base.lang.buildfile.psi.LoadedSymbol;
+import com.google.idea.blaze.base.lang.buildfile.psi.ReferenceExpression;
+import com.google.idea.blaze.base.lang.buildfile.psi.TargetExpression;
 import com.google.idea.blaze.base.lang.buildfile.psi.util.PsiUtils;
+import com.google.idea.blaze.base.lang.buildfile.search.FindUsages;
 import com.google.idea.blaze.base.model.primitives.WorkspacePath;
+import com.intellij.openapi.project.Project;
+import com.intellij.psi.PsiReference;
+import java.util.Arrays;
+import java.util.stream.Collectors;
+import javax.annotation.Nullable;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.junit.runners.JUnit4;
@@ -67,7 +79,7 @@
   //  BuildFile buildFile = createBuildFile(
   //    "java/com/google/tools/BUILD",
   //    "load(",
-  //    "\"//java/com/google/build_defs.bzl\",",
+  //    "\"/java/com/google/build_defs.bzl\",",
   //    "\"function\"",
   //    ")");
   //
@@ -119,6 +131,71 @@
   }
 
   @Test
+  public void testLoadedSymbolReference() {
+    BuildFile extFile =
+        createBuildFile(new WorkspacePath("java/com/google/tools/build_defs.bzl"), "CONSTANT = 1");
+
+    BuildFile buildFile =
+        createBuildFile(
+            new WorkspacePath("java/com/google/BUILD"),
+            "load(",
+            "\"//java/com/google/tools:build_defs.bzl\",",
+            "\"CONSTANT\"",
+            ")",
+            "NEW_CONSTANT = CONSTANT");
+
+    TargetExpression target =
+        PsiUtils.findFirstChildOfClassRecursive(extFile, TargetExpression.class);
+    ReferenceExpression ref =
+        PsiUtils.findFirstChildOfClassRecursive(buildFile, ReferenceExpression.class);
+    LoadedSymbol loadElement =
+        PsiUtils.findFirstChildOfClassRecursive(buildFile, LoadedSymbol.class);
+
+    assertThat(target).isNotNull();
+    assertThat(ref.getReferencedElement()).isEqualTo(target);
+    assertThat(loadElement.getImport().getReferencedElement()).isEqualTo(target);
+
+    assertThat(
+            Arrays.stream(FindUsages.findAllReferences(target))
+                .map(PsiReference::getElement)
+                .collect(Collectors.toList()))
+        .containsExactly(ref, loadElement.getImport());
+  }
+
+  @Test
+  public void testOverridenBuiltInSymbolReference() {
+    setBuiltInRuleNames("java_library");
+    BuildFile extFile =
+        createBuildFile(
+            new WorkspacePath("java/com/google/tools/build_defs.bzl"), "java_library = rule()");
+
+    BuildFile buildFile =
+        createBuildFile(
+            new WorkspacePath("java/com/google/BUILD"),
+            "load(",
+            "\"//java/com/google/tools:build_defs.bzl\",",
+            "\"java_library\"",
+            ")",
+            "java_library(name = 'name')");
+
+    TargetExpression target =
+        PsiUtils.findFirstChildOfClassRecursive(extFile, TargetExpression.class);
+    FuncallExpression funcall = buildFile.firstChildOfClass(FuncallExpression.class);
+    LoadedSymbol loadElement =
+        PsiUtils.findFirstChildOfClassRecursive(buildFile, LoadedSymbol.class);
+
+    assertThat(target).isNotNull();
+    assertThat(funcall.getReferencedElement()).isEqualTo(target);
+    assertThat(loadElement.getImport().getReferencedElement()).isEqualTo(target);
+
+    assertThat(
+            Arrays.stream(FindUsages.findAllReferences(target))
+                .map(PsiReference::getElement)
+                .collect(Collectors.toList()))
+        .containsExactly(funcall, loadElement.getImport());
+  }
+
+  @Test
   public void testFuncallReference() {
     BuildFile extFile =
         createBuildFile(
@@ -181,4 +258,30 @@
     assertThat(function).isNotNull();
     assertThat(funcall.getReferencedElement()).isEqualTo(function);
   }
+
+  private void setBuiltInRuleNames(String... ruleNames) {
+    ImmutableMap.Builder<String, RuleDefinition> rules = ImmutableMap.builder();
+    for (String name : ruleNames) {
+      rules.put(name, new RuleDefinition(name, ImmutableMap.of(), null));
+    }
+    MockBuildLanguageSpecProvider specProvider = new MockBuildLanguageSpecProvider();
+    specProvider.setRules(rules.build());
+    registerApplicationService(BuildLanguageSpecProvider.class, specProvider);
+    specProvider.setRules(rules.build());
+  }
+
+  private static class MockBuildLanguageSpecProvider implements BuildLanguageSpecProvider {
+
+    BuildLanguageSpec languageSpec = new BuildLanguageSpec(ImmutableMap.of());
+
+    void setRules(ImmutableMap<String, RuleDefinition> rules) {
+      languageSpec = new BuildLanguageSpec(rules);
+    }
+
+    @Nullable
+    @Override
+    public BuildLanguageSpec getLanguageSpec(Project project) {
+      return languageSpec;
+    }
+  }
 }
diff --git a/base/tests/integrationtests/com/google/idea/blaze/base/lang/buildfile/references/PackageReferenceTest.java b/base/tests/integrationtests/com/google/idea/blaze/base/lang/buildfile/references/PackageReferenceTest.java
index afb3760..6f09694 100644
--- a/base/tests/integrationtests/com/google/idea/blaze/base/lang/buildfile/references/PackageReferenceTest.java
+++ b/base/tests/integrationtests/com/google/idea/blaze/base/lang/buildfile/references/PackageReferenceTest.java
@@ -25,6 +25,8 @@
 import com.google.idea.blaze.base.lang.buildfile.psi.util.PsiUtils;
 import com.google.idea.blaze.base.model.primitives.WorkspacePath;
 import com.intellij.psi.PsiReference;
+import java.util.Arrays;
+import java.util.stream.Collectors;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.junit.runners.JUnit4;
@@ -76,8 +78,7 @@
         PsiUtils.findFirstChildOfClassRecursive(packagesArg, StringLiteral.class);
 
     PsiReference[] references = string.getReferences();
-    assertThat(references).hasLength(2);
-    assertThat(references[0].resolve()).isEqualTo(libTarget);
-    assertThat(references[1].resolve()).isEqualTo(buildFile1);
+    assertThat(Arrays.stream(references).map(PsiReference::resolve).collect(Collectors.toList()))
+        .containsAllOf(libTarget, buildFile1);
   }
 }
diff --git a/base/tests/integrationtests/com/google/idea/blaze/base/lang/buildfile/validation/BuiltInRuleAnnotatorTest.java b/base/tests/integrationtests/com/google/idea/blaze/base/lang/buildfile/validation/BuiltInRuleAnnotatorTest.java
index 28eece6..baf2d39 100644
--- a/base/tests/integrationtests/com/google/idea/blaze/base/lang/buildfile/validation/BuiltInRuleAnnotatorTest.java
+++ b/base/tests/integrationtests/com/google/idea/blaze/base/lang/buildfile/validation/BuiltInRuleAnnotatorTest.java
@@ -55,6 +55,9 @@
   private static final AttributeDefinition NEVERLINK_ATTRIBUTE =
       new AttributeDefinition("neverlink", Discriminator.BOOLEAN, false, null, null);
 
+  private static final AttributeDefinition VALUES_ATTRIBUTE =
+      new AttributeDefinition("values", Discriminator.STRING_DICT, true, null, null);
+
   private static final RuleDefinition JAVA_TEST =
       new RuleDefinition(
           "java_test",
@@ -62,6 +65,12 @@
               "name", NAME_ATTRIBUTE, "srcs", SRCS_ATTRIBUTE, "neverlink", NEVERLINK_ATTRIBUTE),
           null);
 
+  private static final RuleDefinition CONFIG_SETTING =
+      new RuleDefinition(
+          "config_setting",
+          ImmutableMap.of("name", NAME_ATTRIBUTE, "values", VALUES_ATTRIBUTE),
+          null);
+
   private MockBuildLanguageSpecProvider specProvider;
 
   @Before
@@ -95,6 +104,33 @@
   }
 
   @Test
+  public void testNoErrorsForValidStringDict() {
+    specProvider.setRules(ImmutableMap.of(CONFIG_SETTING.name, CONFIG_SETTING));
+    BuildFile file =
+        createBuildFile(
+            new WorkspacePath("java/com/google/BUILD"),
+            "config_setting(",
+            "    name = 'setting',",
+            "    values = {'key1', 'value1', 'key2', 'value2'},",
+            ")");
+    assertNoErrors(file);
+  }
+
+  @Test
+  public void testErrorForInvalidDict() {
+    specProvider.setRules(ImmutableMap.of(CONFIG_SETTING.name, CONFIG_SETTING));
+    BuildFile file =
+        createBuildFile(
+            new WorkspacePath("java/com/google/BUILD"),
+            "config_setting(",
+            "    name = 'setting',",
+            "    values = 1,",
+            ")");
+    assertHasError(
+        file, "Invalid value for attribute 'values'. Expected a value of type 'STRING_DICT'");
+  }
+
+  @Test
   public void testGlobTreatedAsList() {
     specProvider.setRules(ImmutableMap.of(JAVA_TEST.name, JAVA_TEST));
     BuildFile file =
@@ -239,6 +275,35 @@
     assertNoErrors(file);
   }
 
+  @Test
+  public void testNoMissingMandatoryArgErrorIfKwargsPresent() {
+    specProvider.setRules(ImmutableMap.of(JAVA_TEST.name, JAVA_TEST));
+    BuildFile file =
+        createBuildFile(
+            new WorkspacePath("java/com/google/BUILD"),
+            "def java_test(srcs=[], **kwargs):",
+            "  native.java_test(srcs = srcs, **kwargs)");
+    assertNoErrors(file);
+  }
+
+  @Test
+  public void testNoMissingAttributeErrorsForOverriddenBuiltIns() {
+    specProvider.setRules(ImmutableMap.of(JAVA_TEST.name, JAVA_TEST));
+    BuildFile file =
+        createBuildFile(
+            new WorkspacePath("java/com/foo/BUILD"),
+            "java_test(name = 'test', srcs = [':src'], extra_arg = [])");
+    assertHasError(file, "Unrecognized attribute 'extra_arg' for rule type 'java_test'");
+
+    file =
+        createBuildFile(
+            new WorkspacePath("java/com/bar/BUILD"),
+            "def java_test(srcs=[], **kwargs, extra_arg=[]):",
+            "  native.java_test(srcs = srcs, **kwargs)",
+            "java_test(name = 'test', srcs = [':src'], extra_arg = [])");
+    assertNoErrors(file);
+  }
+
   private void setRules(String... ruleNames) {
     ImmutableMap.Builder<String, RuleDefinition> rules = ImmutableMap.builder();
     for (String name : ruleNames) {
diff --git a/base/tests/integrationtests/com/google/idea/blaze/base/lang/buildfile/validation/LoadStatementAnnotatorTest.java b/base/tests/integrationtests/com/google/idea/blaze/base/lang/buildfile/validation/LoadStatementAnnotatorTest.java
new file mode 100644
index 0000000..ce2f332
--- /dev/null
+++ b/base/tests/integrationtests/com/google/idea/blaze/base/lang/buildfile/validation/LoadStatementAnnotatorTest.java
@@ -0,0 +1,128 @@
+/*
+ * Copyright 2017 The Bazel Authors. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.idea.blaze.base.lang.buildfile.validation;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.google.idea.blaze.base.lang.buildfile.BuildFileIntegrationTestCase;
+import com.google.idea.blaze.base.lang.buildfile.psi.BuildElement;
+import com.google.idea.blaze.base.lang.buildfile.psi.BuildFile;
+import com.google.idea.blaze.base.lang.buildfile.psi.util.PsiUtils;
+import com.google.idea.blaze.base.model.primitives.WorkspacePath;
+import com.intellij.codeInsight.daemon.impl.AnnotationHolderImpl;
+import com.intellij.lang.annotation.Annotation;
+import com.intellij.lang.annotation.AnnotationHolder;
+import com.intellij.lang.annotation.AnnotationSession;
+import com.intellij.lang.annotation.HighlightSeverity;
+import com.intellij.psi.PsiFile;
+import java.util.List;
+import java.util.stream.Collectors;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** Tests for {@link LoadStatementAnnotator}. */
+@RunWith(JUnit4.class)
+public class LoadStatementAnnotatorTest extends BuildFileIntegrationTestCase {
+
+  @Test
+  public void testNoWarningsInNormalLoad() {
+    BuildFile file =
+        createBuildFile(
+            new WorkspacePath("java/com/google/BUILD"),
+            "load('//tools/ide/build_test.bzl', 'build_test')",
+            "load(':local_file.bzl', 'symbol')");
+    assertNoAnnotations(file);
+  }
+
+  @Test
+  public void testNoWarningsInExternalLoad() {
+    BuildFile file =
+        createBuildFile(
+            new WorkspacePath("java/com/google/BUILD"),
+            "load('@tools//ide:build_test.bzl', 'build_test')");
+    assertNoAnnotations(file);
+  }
+
+  @Test
+  public void testNoWarningsWhenTyping() {
+    BuildFile file = createBuildFile(new WorkspacePath("java/com/google/BUILD"), "load('/')");
+    assertNoAnnotations(file);
+  }
+
+  @Test
+  public void testWarningForDeprecatedFormat() {
+    BuildFile file =
+        createBuildFile(
+            new WorkspacePath("java/com/google/BUILD"),
+            "load('/tools/ide/build_test.bzl', 'build_test')");
+    assertHasAnnotation(
+        file,
+        "Deprecated load syntax; loaded Skylark module should by in label format.",
+        HighlightSeverity.WARNING);
+  }
+
+  @Test
+  public void testErrorForUnrecognizedFormat() {
+    BuildFile file =
+        createBuildFile(
+            new WorkspacePath("java/com/google/BUILD"), "load('not a skylark label', 'symbol')");
+    assertHasAnnotation(
+        file, "Invalid load syntax: missing Skylark module.", HighlightSeverity.ERROR);
+  }
+
+  @Test
+  public void testErrorForPrivateSymbols() {
+    BuildFile file =
+        createBuildFile(
+            new WorkspacePath("java/com/google/BUILD"), "load(':skylark.bzl', '_local_symbol')");
+    assertHasAnnotation(
+        file, "Symbol '_local_symbol' is private and cannot be imported.", HighlightSeverity.ERROR);
+  }
+
+  private void assertNoAnnotations(BuildFile file) {
+    assertThat(validateFile(file)).isEmpty();
+  }
+
+  private void assertHasAnnotation(BuildFile file, String message, HighlightSeverity type) {
+    assertThat(
+            validateFile(file)
+                .stream()
+                .filter(ann -> ann.getSeverity().equals(type))
+                .map(Annotation::getMessage)
+                .collect(Collectors.toList()))
+        .contains(message);
+  }
+
+  private List<Annotation> validateFile(BuildFile file) {
+    LoadStatementAnnotator annotator = createAnnotator(file);
+    PsiUtils.findAllChildrenOfClassRecursive(file, BuildElement.class)
+        .forEach(element -> element.accept(annotator));
+    return annotationHolder;
+  }
+
+  private LoadStatementAnnotator createAnnotator(PsiFile file) {
+    annotationHolder = new AnnotationHolderImpl(new AnnotationSession(file));
+    return new LoadStatementAnnotator() {
+      @Override
+      protected AnnotationHolder getHolder() {
+        return annotationHolder;
+      }
+    };
+  }
+
+  private AnnotationHolderImpl annotationHolder = null;
+}
diff --git a/base/tests/integrationtests/com/google/idea/blaze/base/lang/projectview/ProjectViewCompletionTest.java b/base/tests/integrationtests/com/google/idea/blaze/base/lang/projectview/ProjectViewCompletionTest.java
index 99390e2..edf97b7 100644
--- a/base/tests/integrationtests/com/google/idea/blaze/base/lang/projectview/ProjectViewCompletionTest.java
+++ b/base/tests/integrationtests/com/google/idea/blaze/base/lang/projectview/ProjectViewCompletionTest.java
@@ -19,7 +19,6 @@
 
 import com.google.common.base.Joiner;
 import com.google.idea.blaze.base.lang.projectview.completion.ProjectViewKeywordCompletionContributor;
-import com.google.idea.blaze.base.model.primitives.LanguageClass;
 import com.google.idea.blaze.base.model.primitives.WorkspacePath;
 import com.google.idea.blaze.base.model.primitives.WorkspaceType;
 import com.google.idea.blaze.base.projectview.section.SectionParser;
@@ -126,20 +125,6 @@
   }
 
   @Test
-  public void testAdditionalLanguagesCompletion() {
-    setInput("additional_languages:", "  <caret>");
-
-    String[] types = editorTest.getCompletionItemsAsStrings();
-
-    assertThat(types)
-        .asList()
-        .containsAllIn(
-            Arrays.stream(LanguageClass.values())
-                .map(LanguageClass::getName)
-                .collect(Collectors.toList()));
-  }
-
-  @Test
   public void testUniqueDirectoryCompleted() {
     setInput("import <caret>");
 
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 b6b1c80..42be3e9 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
@@ -84,10 +84,10 @@
 
     BlazeCommandRunConfigurationCommonState state =
         (BlazeCommandRunConfigurationCommonState) configuration.getHandler().getState();
-    state.setCommand(COMMAND);
-    state.setBlazeFlags(ImmutableList.of("--flag1", "--flag2"));
-    state.setExeFlags(ImmutableList.of("--exeFlag1"));
-    state.setBlazeBinary("/usr/bin/blaze");
+    state.getCommandState().setCommand(COMMAND);
+    state.getBlazeFlagsState().setRawFlags(ImmutableList.of("--flag1", "--flag2"));
+    state.getExeFlagsState().setRawFlags(ImmutableList.of("--exeFlag1"));
+    state.getBlazeBinaryState().setBlazeBinary("/usr/bin/blaze");
 
     Element element = new Element("test");
     configuration.writeExternal(element);
@@ -101,10 +101,12 @@
 
     BlazeCommandRunConfigurationCommonState readState =
         (BlazeCommandRunConfigurationCommonState) readConfiguration.getHandler().getState();
-    assertThat(readState.getCommand()).isEqualTo(COMMAND);
-    assertThat(readState.getBlazeFlags()).containsExactly("--flag1", "--flag2").inOrder();
-    assertThat(readState.getExeFlags()).containsExactly("--exeFlag1");
-    assertThat(readState.getBlazeBinary()).isEqualTo("/usr/bin/blaze");
+    assertThat(readState.getCommandState().getCommand()).isEqualTo(COMMAND);
+    assertThat(readState.getBlazeFlagsState().getRawFlags())
+        .containsExactly("--flag1", "--flag2")
+        .inOrder();
+    assertThat(readState.getExeFlagsState().getRawFlags()).containsExactly("--exeFlag1");
+    assertThat(readState.getBlazeBinaryState().getBlazeBinary()).isEqualTo("/usr/bin/blaze");
   }
 
   @Test
@@ -129,10 +131,10 @@
 
     BlazeCommandRunConfigurationCommonState state =
         (BlazeCommandRunConfigurationCommonState) configuration.getHandler().getState();
-    state.setCommand(COMMAND);
-    state.setBlazeFlags(ImmutableList.of("--flag1", "--flag2"));
-    state.setExeFlags(ImmutableList.of("--exeFlag1"));
-    state.setBlazeBinary("/usr/bin/blaze");
+    state.getCommandState().setCommand(COMMAND);
+    state.getBlazeFlagsState().setRawFlags(ImmutableList.of("--flag1", "--flag2"));
+    state.getExeFlagsState().setRawFlags(ImmutableList.of("--exeFlag1"));
+    state.getBlazeBinaryState().setBlazeBinary("/usr/bin/blaze");
 
     editor.resetFrom(configuration);
     BlazeCommandRunConfiguration readConfiguration =
@@ -145,10 +147,14 @@
 
     BlazeCommandRunConfigurationCommonState readState =
         (BlazeCommandRunConfigurationCommonState) readConfiguration.getHandler().getState();
-    assertThat(readState.getCommand()).isEqualTo(state.getCommand());
-    assertThat(readState.getBlazeFlags()).isEqualTo(state.getBlazeFlags());
-    assertThat(readState.getExeFlags()).isEqualTo(state.getExeFlags());
-    assertThat(readState.getBlazeBinary()).isEqualTo(state.getBlazeBinary());
+    assertThat(readState.getCommandState().getCommand())
+        .isEqualTo(state.getCommandState().getCommand());
+    assertThat(readState.getBlazeFlagsState().getRawFlags())
+        .isEqualTo(state.getBlazeFlagsState().getRawFlags());
+    assertThat(readState.getExeFlagsState().getRawFlags())
+        .isEqualTo(state.getExeFlagsState().getRawFlags());
+    assertThat(readState.getBlazeBinaryState().getBlazeBinary())
+        .isEqualTo(state.getBlazeBinaryState().getBlazeBinary());
 
     Disposer.dispose(editor);
   }
@@ -175,10 +181,10 @@
 
     BlazeCommandRunConfigurationCommonState readState =
         (BlazeCommandRunConfigurationCommonState) readConfiguration.getHandler().getState();
-    readState.setCommand(COMMAND);
-    readState.setBlazeFlags(ImmutableList.of("--flag1", "--flag2"));
-    readState.setExeFlags(ImmutableList.of("--exeFlag1"));
-    readState.setBlazeBinary("/usr/bin/blaze");
+    readState.getCommandState().setCommand(COMMAND);
+    readState.getBlazeFlagsState().setRawFlags(ImmutableList.of("--flag1", "--flag2"));
+    readState.getExeFlagsState().setRawFlags(ImmutableList.of("--exeFlag1"));
+    readState.getBlazeBinaryState().setBlazeBinary("/usr/bin/blaze");
 
     editor.applyEditorTo(readConfiguration);
 
@@ -187,10 +193,14 @@
         .isInstanceOf(BlazeCommandGenericRunConfigurationHandler.class);
 
     readState = (BlazeCommandRunConfigurationCommonState) readConfiguration.getHandler().getState();
-    assertThat(readState.getCommand()).isEqualTo(state.getCommand());
-    assertThat(readState.getBlazeFlags()).isEqualTo(state.getBlazeFlags());
-    assertThat(readState.getExeFlags()).isEqualTo(state.getExeFlags());
-    assertThat(readState.getBlazeBinary()).isEqualTo(state.getBlazeBinary());
+    assertThat(readState.getCommandState().getCommand())
+        .isEqualTo(state.getCommandState().getCommand());
+    assertThat(readState.getBlazeFlagsState().getRawFlags())
+        .isEqualTo(state.getBlazeFlagsState().getRawFlags());
+    assertThat(readState.getExeFlagsState().getRawFlags())
+        .isEqualTo(state.getExeFlagsState().getRawFlags());
+    assertThat(readState.getBlazeBinaryState().getBlazeBinary())
+        .isEqualTo(state.getBlazeBinaryState().getBlazeBinary());
 
     Disposer.dispose(editor);
   }
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 d1e3b7f..715cbc0 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
@@ -77,7 +77,7 @@
 
   @Test
   public void loadStateAndGetStateShouldMatch() {
-    final Label label = new Label("//package:rule");
+    final Label label = Label.create("//package:rule");
     configuration.setTarget(label);
 
     final Element element = runManager.getState();
@@ -94,7 +94,7 @@
   @Test
   public void loadStateAndGetStateElementShouldMatch() {
     final XMLOutputter xmlOutputter = new XMLOutputter(Format.getCompactFormat());
-    configuration.setTarget(new Label("//package:rule"));
+    configuration.setTarget(Label.create("//package:rule"));
 
     final Element initialElement = runManager.getState();
     runManager.loadState(initialElement);
@@ -107,14 +107,14 @@
   @Test
   public void loadStateAndGetStateElementShouldMatchAfterChangeAndRevert() {
     final XMLOutputter xmlOutputter = new XMLOutputter(Format.getCompactFormat());
-    final Label label = new Label("//package:rule");
+    final Label label = Label.create("//package:rule");
     configuration.setTarget(label);
 
     final Element initialElement = runManager.getState();
     runManager.loadState(initialElement);
     final BlazeCommandRunConfiguration modifiedConfiguration =
         (BlazeCommandRunConfiguration) runManager.getAllConfigurations()[0];
-    modifiedConfiguration.setTarget(new Label("//new:label"));
+    modifiedConfiguration.setTarget(Label.create("//new:label"));
 
     final Element modifiedElement = runManager.getState();
     assertThat(xmlOutputter.outputString(modifiedElement))
@@ -135,7 +135,7 @@
     final XMLOutputter xmlOutputter = new XMLOutputter(Format.getCompactFormat());
     final BlazeCommandRunConfigurationSettingsEditor editor =
         new BlazeCommandRunConfigurationSettingsEditor(configuration);
-    configuration.setTarget(new Label("//package:rule"));
+    configuration.setTarget(Label.create("//package:rule"));
 
     final Element initialElement = runManager.getState();
     editor.resetFrom(configuration);
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 61b71f5..66984ae 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
@@ -51,7 +51,7 @@
   public void testEditorApplyToAndResetFromMatches() throws ConfigurationException {
     BlazeCommandRunConfigurationSettingsEditor editor =
         new BlazeCommandRunConfigurationSettingsEditor(configuration);
-    Label label = new Label("//package:rule");
+    Label label = Label.create("//package:rule");
     configuration.setTarget(label);
 
     editor.resetFrom(configuration);
diff --git a/base/tests/integrationtests/com/google/idea/blaze/base/run/TestTargetHeuristicTest.java b/base/tests/integrationtests/com/google/idea/blaze/base/run/TestTargetHeuristicTest.java
index 6cd2d7c..9732eda 100644
--- a/base/tests/integrationtests/com/google/idea/blaze/base/run/TestTargetHeuristicTest.java
+++ b/base/tests/integrationtests/com/google/idea/blaze/base/run/TestTargetHeuristicTest.java
@@ -19,12 +19,19 @@
 
 import com.google.common.collect.ImmutableList;
 import com.google.idea.blaze.base.BlazeIntegrationTestCase;
+import com.google.idea.blaze.base.ideinfo.ArtifactLocation;
 import com.google.idea.blaze.base.ideinfo.TargetIdeInfo;
 import com.google.idea.blaze.base.ideinfo.TestIdeInfo;
 import com.google.idea.blaze.base.ideinfo.TestIdeInfo.TestSize;
+import com.google.idea.blaze.base.model.BlazeProjectData;
+import com.google.idea.blaze.base.model.MockBlazeProjectDataBuilder;
+import com.google.idea.blaze.base.model.MockBlazeProjectDataManager;
 import com.google.idea.blaze.base.model.primitives.Label;
+import com.google.idea.blaze.base.model.primitives.WorkspacePath;
+import com.google.idea.blaze.base.sync.data.BlazeProjectDataManager;
 import java.io.File;
 import java.util.Collection;
+import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.junit.runners.JUnit4;
@@ -33,9 +40,16 @@
 @RunWith(JUnit4.class)
 public class TestTargetHeuristicTest extends BlazeIntegrationTestCase {
 
+  @Before
+  public final void doSetup() {
+    BlazeProjectData blazeProjectData = MockBlazeProjectDataBuilder.builder(workspaceRoot).build();
+    registerProjectService(
+        BlazeProjectDataManager.class, new MockBlazeProjectDataManager(blazeProjectData));
+  }
+
   @Test
-  public void testTestSizeMatched() throws Exception {
-    File source = new File("java/com/foo/FooTest.java");
+  public void testTestSizeMatched() {
+    File source = workspaceRoot.fileForPath(new WorkspacePath("java/com/foo/FooTest.java"));
     Collection<TargetIdeInfo> targets =
         ImmutableList.of(
             TargetIdeInfo.builder()
@@ -49,24 +63,48 @@
                 .setTestInfo(TestIdeInfo.builder().setTestSize(TestSize.SMALL))
                 .build());
     Label match =
-        TestTargetHeuristic.chooseTestTargetForSourceFile(source, targets, TestSize.SMALL);
-    assertThat(match).isEqualTo(new Label("//foo:test2"));
+        TestTargetHeuristic.chooseTestTargetForSourceFile(
+            getProject(), null, source, targets, TestSize.SMALL);
+    assertThat(match).isEqualTo(Label.create("//foo:test2"));
   }
 
   @Test
-  public void testTargetNameMatched() throws Exception {
-    File source = new File("java/com/foo/FooTest.java");
+  public void testTargetSourcesMatched() {
+    File source = workspaceRoot.fileForPath(new WorkspacePath("java/com/foo/FooTest.java"));
+    Collection<TargetIdeInfo> targets =
+        ImmutableList.of(
+            TargetIdeInfo.builder()
+                .setLabel("//foo:test1")
+                .setKind("java_test")
+                .addSource(sourceRoot("java/com/bar/OtherTest.java"))
+                .build(),
+            TargetIdeInfo.builder()
+                .setLabel("//foo:test2")
+                .setKind("java_test")
+                .addSource(sourceRoot("java/com/foo/FooTest.java"))
+                .build());
+    Label match =
+        TestTargetHeuristic.chooseTestTargetForSourceFile(
+            getProject(), null, source, targets, null);
+    assertThat(match).isEqualTo(Label.create("//foo:test2"));
+  }
+
+  @Test
+  public void testTargetNameMatched() {
+    File source = workspaceRoot.fileForPath(new WorkspacePath("java/com/foo/FooTest.java"));
     Collection<TargetIdeInfo> targets =
         ImmutableList.of(
             TargetIdeInfo.builder().setLabel("//foo:FirstTest").setKind("java_test").build(),
             TargetIdeInfo.builder().setLabel("//foo:FooTest").setKind("java_test").build());
-    Label match = TestTargetHeuristic.chooseTestTargetForSourceFile(source, targets, null);
-    assertThat(match).isEqualTo(new Label("//foo:FooTest"));
+    Label match =
+        TestTargetHeuristic.chooseTestTargetForSourceFile(
+            getProject(), null, source, targets, null);
+    assertThat(match).isEqualTo(Label.create("//foo:FooTest"));
   }
 
   @Test
-  public void testNoMatchFallBackToFirstTarget() throws Exception {
-    File source = new File("java/com/foo/FooTest.java");
+  public void testNoMatchFallBackToFirstTarget() {
+    File source = workspaceRoot.fileForPath(new WorkspacePath("java/com/foo/FooTest.java"));
     ImmutableList<TargetIdeInfo> targets =
         ImmutableList.of(
             TargetIdeInfo.builder()
@@ -80,13 +118,14 @@
                 .setTestInfo(TestIdeInfo.builder().setTestSize(TestSize.SMALL))
                 .build());
     Label match =
-        TestTargetHeuristic.chooseTestTargetForSourceFile(source, targets, TestSize.LARGE);
-    assertThat(match).isEqualTo(new Label("//bar:BarTest"));
+        TestTargetHeuristic.chooseTestTargetForSourceFile(
+            getProject(), null, source, targets, TestSize.LARGE);
+    assertThat(match).isEqualTo(Label.create("//bar:BarTest"));
   }
 
   @Test
-  public void testTargetNameCheckedBeforeTestSize() throws Exception {
-    File source = new File("java/com/foo/FooTest.java");
+  public void testTargetNameCheckedBeforeTestSize() {
+    File source = workspaceRoot.fileForPath(new WorkspacePath("java/com/foo/FooTest.java"));
     ImmutableList<TargetIdeInfo> targets =
         ImmutableList.of(
             TargetIdeInfo.builder()
@@ -100,7 +139,35 @@
                 .setTestInfo(TestIdeInfo.builder().setTestSize(TestSize.MEDIUM))
                 .build());
     Label match =
-        TestTargetHeuristic.chooseTestTargetForSourceFile(source, targets, TestSize.SMALL);
-    assertThat(match).isEqualTo(new Label("//foo:FooTest"));
+        TestTargetHeuristic.chooseTestTargetForSourceFile(
+            getProject(), null, source, targets, TestSize.SMALL);
+    assertThat(match).isEqualTo(Label.create("//foo:FooTest"));
+  }
+
+  @Test
+  public void testTargetSourcesCheckedBeforeTestSize() {
+    File source = workspaceRoot.fileForPath(new WorkspacePath("java/com/foo/FooTest.java"));
+    Collection<TargetIdeInfo> targets =
+        ImmutableList.of(
+            TargetIdeInfo.builder()
+                .setLabel("//foo:test1")
+                .setKind("java_test")
+                .setTestInfo(TestIdeInfo.builder().setTestSize(TestSize.SMALL))
+                .addSource(sourceRoot("java/com/bar/OtherTest.java"))
+                .build(),
+            TargetIdeInfo.builder()
+                .setLabel("//foo:test2")
+                .setKind("java_test")
+                .setTestInfo(TestIdeInfo.builder().setTestSize(TestSize.MEDIUM))
+                .addSource(sourceRoot("java/com/foo/FooTest.java"))
+                .build());
+    Label match =
+        TestTargetHeuristic.chooseTestTargetForSourceFile(
+            getProject(), null, source, targets, TestSize.SMALL);
+    assertThat(match).isEqualTo(Label.create("//foo:test2"));
+  }
+
+  private static ArtifactLocation sourceRoot(String relativePath) {
+    return ArtifactLocation.builder().setRelativePath(relativePath).setIsSource(true).build();
   }
 }
diff --git a/base/tests/integrationtests/com/google/idea/blaze/base/run/exporter/RunConfigurationSerializerTest.java b/base/tests/integrationtests/com/google/idea/blaze/base/run/exporter/RunConfigurationSerializerTest.java
index 775e65f..0a28d06 100644
--- 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
@@ -80,7 +80,7 @@
 
   @Test
   public void testRunConfigurationUnalteredBySerializationRoundTrip() throws InvalidDataException {
-    configuration.setTarget(new Label("//package:rule"));
+    configuration.setTarget(Label.create("//package:rule"));
     configuration.setKeepInSync(true);
 
     final Element initialElement = runManager.getState();
@@ -99,7 +99,7 @@
 
   @Test
   public void testSetKeepInSyncWhenImporting() throws InvalidDataException {
-    configuration.setTarget(new Label("//package:rule"));
+    configuration.setTarget(Label.create("//package:rule"));
     configuration.setKeepInSync(false);
 
     Element element = RunConfigurationSerializer.writeToXml(configuration);
diff --git a/base/tests/integrationtests/com/google/idea/blaze/base/run/producer/AllInPackageBlazeConfigurationProducerTest.java b/base/tests/integrationtests/com/google/idea/blaze/base/run/producer/AllInPackageBlazeConfigurationProducerTest.java
new file mode 100644
index 0000000..7063c58
--- /dev/null
+++ b/base/tests/integrationtests/com/google/idea/blaze/base/run/producer/AllInPackageBlazeConfigurationProducerTest.java
@@ -0,0 +1,75 @@
+/*
+ * Copyright 2017 The Bazel Authors. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.idea.blaze.base.run.producer;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.google.idea.blaze.base.command.BlazeCommandName;
+import com.google.idea.blaze.base.model.primitives.TargetExpression;
+import com.google.idea.blaze.base.model.primitives.WorkspacePath;
+import com.google.idea.blaze.base.run.BlazeCommandRunConfiguration;
+import com.google.idea.blaze.base.run.producers.AllInPackageBlazeConfigurationProducer;
+import com.intellij.execution.actions.ConfigurationContext;
+import com.intellij.execution.actions.ConfigurationFromContext;
+import com.intellij.psi.PsiDirectory;
+import java.util.List;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** Integration tests for {@link AllInPackageBlazeConfigurationProducer}. */
+@RunWith(JUnit4.class)
+public class AllInPackageBlazeConfigurationProducerTest
+    extends BlazeRunConfigurationProducerTestCase {
+
+  @Test
+  public void testProducedFromPsiDirectory() {
+    PsiDirectory directory =
+        workspace.createPsiDirectory(new WorkspacePath("java/com/google/test"));
+    workspace.createPsiFile(
+        new WorkspacePath("java/com/google/test/BUILD"), "java_test(name='unit_tests'");
+
+    ConfigurationContext context = createContextFromPsi(directory);
+    List<ConfigurationFromContext> configurations = context.getConfigurationsFromContext();
+    assertThat(configurations).hasSize(1);
+
+    ConfigurationFromContext fromContext = configurations.get(0);
+    assertThat(fromContext.isProducedBy(AllInPackageBlazeConfigurationProducer.class)).isTrue();
+    assertThat(fromContext.getConfiguration()).isInstanceOf(BlazeCommandRunConfiguration.class);
+
+    BlazeCommandRunConfiguration config =
+        (BlazeCommandRunConfiguration) fromContext.getConfiguration();
+    assertThat(config.getTarget())
+        .isEqualTo(TargetExpression.fromString("//java/com/google/test/...:all"));
+    assertThat(getCommandType(config)).isEqualTo(BlazeCommandName.TEST);
+  }
+
+  @Test
+  public void testDirectoryWithoutBlazePackageChildIsIgnored() {
+    PsiDirectory directory =
+        workspace.createPsiDirectory(new WorkspacePath("java/com/google/test"));
+
+    ConfigurationContext context = createContextFromPsi(directory);
+
+    AllInPackageBlazeConfigurationProducer producer = new AllInPackageBlazeConfigurationProducer();
+    assertThat(producer.createConfigurationFromContext(context)).isNull();
+
+    workspace.createPsiDirectory(new WorkspacePath("java/com/google/test/child_dir"));
+    workspace.createPsiFile(new WorkspacePath("java/com/google/test/child_dir/BUILD"));
+
+    assertThat(producer.createConfigurationFromContext(context)).isNotNull();
+  }
+}
diff --git a/base/tests/integrationtests/com/google/idea/blaze/base/run/producer/BlazeBuildFileRunConfigurationProducerTest.java b/base/tests/integrationtests/com/google/idea/blaze/base/run/producer/BlazeBuildFileRunConfigurationProducerTest.java
new file mode 100644
index 0000000..dcf3e6b
--- /dev/null
+++ b/base/tests/integrationtests/com/google/idea/blaze/base/run/producer/BlazeBuildFileRunConfigurationProducerTest.java
@@ -0,0 +1,90 @@
+/*
+ * Copyright 2017 The Bazel Authors. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.idea.blaze.base.run.producer;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.google.idea.blaze.base.command.BlazeCommandName;
+import com.google.idea.blaze.base.lang.buildfile.psi.FuncallExpression;
+import com.google.idea.blaze.base.lang.buildfile.psi.StringLiteral;
+import com.google.idea.blaze.base.lang.buildfile.psi.util.PsiUtils;
+import com.google.idea.blaze.base.model.primitives.TargetExpression;
+import com.google.idea.blaze.base.model.primitives.WorkspacePath;
+import com.google.idea.blaze.base.run.BlazeCommandRunConfiguration;
+import com.google.idea.blaze.base.run.producers.BlazeBuildFileRunConfigurationProducer;
+import com.intellij.execution.actions.ConfigurationContext;
+import com.intellij.execution.actions.ConfigurationFromContext;
+import com.intellij.psi.PsiFile;
+import java.util.List;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** Integration tests for {@link BlazeBuildFileRunConfigurationProducer}. */
+@RunWith(JUnit4.class)
+public class BlazeBuildFileRunConfigurationProducerTest
+    extends BlazeRunConfigurationProducerTestCase {
+
+  @Test
+  public void testProducedFromFuncallExpression() {
+    PsiFile buildFile =
+        workspace.createPsiFile(
+            new WorkspacePath("java/com/google/test/BUILD"), "java_test(name='unit_tests'");
+
+    FuncallExpression target =
+        PsiUtils.findFirstChildOfClassRecursive(buildFile, FuncallExpression.class);
+    assertThat(target).isNotNull();
+
+    ConfigurationContext context = createContextFromPsi(target);
+    List<ConfigurationFromContext> configurations = context.getConfigurationsFromContext();
+    assertThat(configurations).hasSize(1);
+
+    ConfigurationFromContext fromContext = configurations.get(0);
+    assertThat(fromContext.isProducedBy(BlazeBuildFileRunConfigurationProducer.class)).isTrue();
+    assertThat(fromContext.getConfiguration()).isInstanceOf(BlazeCommandRunConfiguration.class);
+
+    BlazeCommandRunConfiguration config =
+        (BlazeCommandRunConfiguration) fromContext.getConfiguration();
+    assertThat(config.getTarget())
+        .isEqualTo(TargetExpression.fromString("//java/com/google/test:unit_tests"));
+    assertThat(getCommandType(config)).isEqualTo(BlazeCommandName.TEST);
+  }
+
+  @Test
+  public void testProducedWhenInsideFuncallExpression() {
+    PsiFile buildFile =
+        workspace.createPsiFile(
+            new WorkspacePath("java/com/google/test/BUILD"), "java_test(name='unit_tests'");
+
+    StringLiteral nameString =
+        PsiUtils.findFirstChildOfClassRecursive(buildFile, StringLiteral.class);
+    assertThat(nameString).isNotNull();
+
+    ConfigurationContext context = createContextFromPsi(nameString);
+    List<ConfigurationFromContext> configurations = context.getConfigurationsFromContext();
+    assertThat(configurations).hasSize(1);
+
+    ConfigurationFromContext fromContext = configurations.get(0);
+    assertThat(fromContext.isProducedBy(BlazeBuildFileRunConfigurationProducer.class)).isTrue();
+    assertThat(fromContext.getConfiguration()).isInstanceOf(BlazeCommandRunConfiguration.class);
+
+    BlazeCommandRunConfiguration config =
+        (BlazeCommandRunConfiguration) fromContext.getConfiguration();
+    assertThat(config.getTarget())
+        .isEqualTo(TargetExpression.fromString("//java/com/google/test:unit_tests"));
+    assertThat(getCommandType(config)).isEqualTo(BlazeCommandName.TEST);
+  }
+}
diff --git a/base/tests/integrationtests/com/google/idea/blaze/base/sync/ImportRootsTest.java b/base/tests/integrationtests/com/google/idea/blaze/base/sync/ImportRootsTest.java
index 84bf00f..df9a08f 100644
--- a/base/tests/integrationtests/com/google/idea/blaze/base/sync/ImportRootsTest.java
+++ b/base/tests/integrationtests/com/google/idea/blaze/base/sync/ImportRootsTest.java
@@ -87,8 +87,18 @@
             .add(new DirectoryEntry(new WorkspacePath(""), true))
             .build();
 
-    assertThat(importRoots.importAsSource(new Label("//:target"))).isTrue();
-    assertThat(importRoots.importAsSource(new Label("//foo/bar:target"))).isTrue();
+    assertThat(importRoots.importAsSource(Label.create("//:target"))).isTrue();
+    assertThat(importRoots.importAsSource(Label.create("//foo/bar:target"))).isTrue();
+  }
+
+  @Test
+  public void testExternalWorkspaceLabelsNotIncludedUnderWorkspaceRoot() {
+    ImportRoots importRoots =
+        ImportRoots.builder(workspaceRoot, BuildSystem.Blaze)
+            .add(new DirectoryEntry(new WorkspacePath(""), true))
+            .build();
+
+    assertThat(importRoots.importAsSource(Label.create("@lib//:target"))).isFalse();
   }
 
   @Test
@@ -138,4 +148,55 @@
             .build();
     assertThat(importRoots.excludeDirectories()).containsExactly(new WorkspacePath("root"));
   }
+
+  @Test
+  public void testContainsWorkspacePath_samePath() throws Exception {
+    ImportRoots importRoots =
+        ImportRoots.builder(workspaceRoot, BuildSystem.Blaze)
+            .add(DirectoryEntry.include(new WorkspacePath("root")))
+            .build();
+
+    assertThat(importRoots.containsWorkspacePath(new WorkspacePath("root"))).isTrue();
+  }
+
+  @Test
+  public void testContainsWorkspacePath_subdirectory() throws Exception {
+    ImportRoots importRoots =
+        ImportRoots.builder(workspaceRoot, BuildSystem.Blaze)
+            .add(DirectoryEntry.include(new WorkspacePath("root")))
+            .build();
+
+    assertThat(importRoots.containsWorkspacePath(new WorkspacePath("root/subdir"))).isTrue();
+  }
+
+  @Test
+  public void testContainsWorkspacePath_differentRoot() throws Exception {
+    ImportRoots importRoots =
+        ImportRoots.builder(workspaceRoot, BuildSystem.Blaze)
+            .add(DirectoryEntry.include(new WorkspacePath("root")))
+            .build();
+
+    assertThat(importRoots.containsWorkspacePath(new WorkspacePath("otherroot"))).isFalse();
+  }
+
+  @Test
+  public void testContainsWorkspacePath_similarRoot() throws Exception {
+    ImportRoots importRoots =
+        ImportRoots.builder(workspaceRoot, BuildSystem.Blaze)
+            .add(DirectoryEntry.include(new WorkspacePath("root")))
+            .build();
+
+    assertThat(importRoots.containsWorkspacePath(new WorkspacePath("root2/subdir"))).isFalse();
+  }
+
+  @Test
+  public void testContainsWorkspacePath_excludedParentsAreHandled() throws Exception {
+    ImportRoots importRoots =
+        ImportRoots.builder(workspaceRoot, BuildSystem.Blaze)
+            .add(DirectoryEntry.include(new WorkspacePath("root")))
+            .add(DirectoryEntry.exclude(new WorkspacePath("root/a")))
+            .build();
+
+    assertThat(importRoots.containsWorkspacePath(new WorkspacePath("root/a/b"))).isFalse();
+  }
 }
diff --git a/base/tests/unittests/com/google/idea/blaze/base/actions/BlazeBuildServiceTest.java b/base/tests/unittests/com/google/idea/blaze/base/actions/BlazeBuildServiceTest.java
index cce3237..23e40c8 100644
--- a/base/tests/unittests/com/google/idea/blaze/base/actions/BlazeBuildServiceTest.java
+++ b/base/tests/unittests/com/google/idea/blaze/base/actions/BlazeBuildServiceTest.java
@@ -59,9 +59,9 @@
 
   @Override
   protected void initTest(Container applicationServices, Container projectServices) {
-    BlazeImportSettingsManager importSettingsManager = new BlazeImportSettingsManager(project);
+    BlazeImportSettingsManager importSettingsManager = new BlazeImportSettingsManager();
     importSettingsManager.setImportSettings(
-        new BlazeImportSettings("", "", "", "", "", Blaze.BuildSystem.Blaze));
+        new BlazeImportSettings("", "", "", "", Blaze.BuildSystem.Blaze));
     projectServices.register(BlazeImportSettingsManager.class, importSettingsManager);
 
     ProjectView view =
@@ -91,7 +91,7 @@
   @Test
   public void testBuildFile() {
     ImmutableCollection<Label> labels =
-        ImmutableList.of(new Label("//foo:bar"), new Label("//foo:baz"));
+        ImmutableList.of(Label.create("//foo:bar"), Label.create("//foo:baz"));
     List<TargetExpression> targets = Lists.newArrayList(labels);
     service.buildFile(project, "Foo.java", labels);
     verify(service).buildTargetExpressions(eq(project), eq(targets), eq(viewSet), any());
diff --git a/base/tests/unittests/com/google/idea/blaze/base/command/BlazeCommandTest.java b/base/tests/unittests/com/google/idea/blaze/base/command/BlazeCommandTest.java
index 64b6894..30f41fa 100644
--- a/base/tests/unittests/com/google/idea/blaze/base/command/BlazeCommandTest.java
+++ b/base/tests/unittests/com/google/idea/blaze/base/command/BlazeCommandTest.java
@@ -21,7 +21,6 @@
 import com.google.idea.blaze.base.BlazeTestCase;
 import com.google.idea.blaze.base.model.primitives.Label;
 import com.google.idea.blaze.base.model.primitives.TargetExpression;
-import com.google.idea.blaze.base.settings.Blaze.BuildSystem;
 import com.google.idea.blaze.base.settings.BlazeUserSettings;
 import com.google.idea.common.experiments.ExperimentService;
 import com.google.idea.common.experiments.MockExperimentService;
@@ -47,8 +46,8 @@
   @Test
   public void addedFlagsShouldGoAtStart() {
     List<String> flagsCommand =
-        BlazeCommand.builder(BuildSystem.Blaze, BlazeCommandName.RUN)
-            .addTargets(new Label("//a:b"))
+        BlazeCommand.builder("/usr/bin/blaze", BlazeCommandName.RUN)
+            .addTargets(Label.create("//a:b"))
             .addBlazeFlags("--flag1", "--flag2")
             .addExeFlags("--exeFlag1", "--exeFlag2")
             .build()
@@ -60,8 +59,8 @@
   @Test
   public void targetsShouldGoAfterBlazeFlagsAndDoubleHyphen() {
     List<String> command =
-        BlazeCommand.builder(BuildSystem.Blaze, BlazeCommandName.RUN)
-            .addTargets(new Label("//a:b"), new Label("//c:d"))
+        BlazeCommand.builder("/usr/bin/blaze", BlazeCommandName.RUN)
+            .addTargets(Label.create("//a:b"), Label.create("//c:d"))
             .addBlazeFlags("--flag1", "--flag2")
             .addExeFlags("--exeFlag1", "--exeFlag2")
             .build()
@@ -75,8 +74,8 @@
   @Test
   public void exeFlagsShouldGoLast() {
     List<String> command =
-        BlazeCommand.builder(BuildSystem.Blaze, BlazeCommandName.RUN)
-            .addTargets(new Label("//a:b"), new Label("//c:d"))
+        BlazeCommand.builder("/usr/bin/blaze", BlazeCommandName.RUN)
+            .addTargets(Label.create("//a:b"), Label.create("//c:d"))
             .addBlazeFlags("--flag1", "--flag2")
             .addExeFlags("--exeFlag1", "--exeFlag2")
             .build()
@@ -88,9 +87,9 @@
   @Test
   public void maintainUserOrderingOfTargets() {
     List<String> command =
-        BlazeCommand.builder(BuildSystem.Blaze, BlazeCommandName.RUN)
+        BlazeCommand.builder("/usr/bin/blaze", BlazeCommandName.RUN)
             .addTargets(
-                new Label("//a:b"), TargetExpression.fromString("-//e:f"), new Label("//c:d"))
+                Label.create("//a:b"), TargetExpression.fromString("-//e:f"), Label.create("//c:d"))
             .addBlazeFlags("--flag1", "--flag2")
             .addExeFlags("--exeFlag1", "--exeFlag2")
             .build()
@@ -116,7 +115,7 @@
   @Test
   public void binaryAndCommandShouldComeFirst() {
     List<String> command =
-        BlazeCommand.builder(BuildSystem.Blaze, BlazeCommandName.BUILD)
+        BlazeCommand.builder("/usr/bin/blaze", BlazeCommandName.BUILD)
             .addBlazeFlags("--flag")
             .addExeFlags("--exeFlag")
             .build()
diff --git a/base/tests/unittests/com/google/idea/blaze/base/issueparser/BlazeIssueParserTest.java b/base/tests/unittests/com/google/idea/blaze/base/issueparser/BlazeIssueParserTest.java
index 437822a..6ed7193 100644
--- a/base/tests/unittests/com/google/idea/blaze/base/issueparser/BlazeIssueParserTest.java
+++ b/base/tests/unittests/com/google/idea/blaze/base/issueparser/BlazeIssueParserTest.java
@@ -77,6 +77,7 @@
             new BlazeIssueParser.CompileParser(workspaceRoot),
             new BlazeIssueParser.TracebackParser(),
             new BlazeIssueParser.BuildParser(),
+            new BlazeIssueParser.SkylarkErrorParser(),
             new BlazeIssueParser.LinelessBuildParser(),
             new BlazeIssueParser.ProjectViewLabelParser(projectViewSet),
             new BlazeIssueParser.InvalidTargetProjectViewPackageParser(
@@ -150,6 +151,22 @@
   }
 
   @Test
+  public void testParseSkylarkError() {
+    BlazeIssueParser blazeIssueParser = new BlazeIssueParser(parsers);
+    IssueOutput issue =
+        blazeIssueParser.parseIssue(
+            "ERROR: /root/third_party/bazel/tools/ide/intellij_info_impl.bzl:42:12: "
+                + "Variable artifact_location is read only");
+    assertNotNull(issue);
+    assertThat(issue.getFile().getPath())
+        .isEqualTo("/root/third_party/bazel/tools/ide/intellij_info_impl.bzl");
+    assertThat(issue.getLine()).isEqualTo(42);
+    assertThat(issue.getColumn()).isEqualTo(12);
+    assertThat(issue.getMessage()).isEqualTo("Variable artifact_location is read only");
+    assertThat(issue.getCategory()).isEqualTo(IssueOutput.Category.ERROR);
+  }
+
+  @Test
   public void testParseLinelessBuildError() {
     BlazeIssueParser blazeIssueParser = new BlazeIssueParser(parsers);
     IssueOutput issue =
diff --git a/base/tests/unittests/com/google/idea/blaze/base/model/blaze/deepequalstester/DeepEqualsTesterUtil.java b/base/tests/unittests/com/google/idea/blaze/base/model/blaze/deepequalstester/DeepEqualsTesterUtil.java
index 2ef9472..2f2457b 100644
--- a/base/tests/unittests/com/google/idea/blaze/base/model/blaze/deepequalstester/DeepEqualsTesterUtil.java
+++ b/base/tests/unittests/com/google/idea/blaze/base/model/blaze/deepequalstester/DeepEqualsTesterUtil.java
@@ -23,8 +23,8 @@
 import java.io.ObjectOutputStream;
 import java.lang.reflect.Field;
 import java.util.List;
+import javax.annotation.Nullable;
 import org.jetbrains.annotations.NotNull;
-import org.jetbrains.annotations.Nullable;
 
 /** Utilities for deep equals testing. */
 @VisibleForTesting
diff --git a/base/tests/unittests/com/google/idea/blaze/base/model/primitives/LabelTest.java b/base/tests/unittests/com/google/idea/blaze/base/model/primitives/LabelTest.java
index 4ceb066..9227a6b 100644
--- a/base/tests/unittests/com/google/idea/blaze/base/model/primitives/LabelTest.java
+++ b/base/tests/unittests/com/google/idea/blaze/base/model/primitives/LabelTest.java
@@ -68,4 +68,47 @@
     assertThat(Label.validate("foo")).isFalse();
     assertThat(Label.validate("foo:bar")).isFalse();
   }
+
+  @Test
+  public void testFactoryMethod() {
+    String fullLabel = "//package/path:target/name";
+    Label label = Label.create(fullLabel);
+    assertThat(label.toString()).isEqualTo(fullLabel);
+    assertThat(label.blazePackage()).isEqualTo(new WorkspacePath("package/path"));
+    assertThat(label.targetName()).isEqualTo(TargetName.create("target/name"));
+  }
+
+  @Test
+  public void testFactoryMethodExternalWorkspace() {
+    String fullLabel = "@ext_workspace//package/path:target/name";
+    Label label = Label.create(fullLabel);
+    assertThat(label.toString()).isEqualTo(fullLabel);
+    assertThat(label.externalWorkspaceName()).isEqualTo("ext_workspace");
+    assertThat(label.blazePackage()).isEqualTo(new WorkspacePath("package/path"));
+    assertThat(label.targetName()).isEqualTo(TargetName.create("target/name"));
+  }
+
+  @Test
+  public void testConstructor() {
+    String externalWorkspaceName = "ext_workspace";
+    WorkspacePath packagePath = new WorkspacePath("package/path");
+    TargetName targetName = TargetName.create("target/name");
+
+    Label label = Label.create(externalWorkspaceName, packagePath, targetName);
+    assertThat(label.toString()).isEqualTo("@ext_workspace//package/path:target/name");
+    assertThat(label.externalWorkspaceName()).isEqualTo(externalWorkspaceName);
+    assertThat(label.blazePackage()).isEqualTo(packagePath);
+    assertThat(label.targetName()).isEqualTo(targetName);
+  }
+
+  @Test
+  public void testConstructorExternalWorkspace() {
+    WorkspacePath packagePath = new WorkspacePath("package/path");
+    TargetName targetName = TargetName.create("target/name");
+
+    Label label = Label.create(packagePath, targetName);
+    assertThat(label.toString()).isEqualTo("//package/path:target/name");
+    assertThat(label.blazePackage()).isEqualTo(packagePath);
+    assertThat(label.targetName()).isEqualTo(targetName);
+  }
 }
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 7758293..9f1ad20 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
@@ -40,6 +40,7 @@
 import com.google.idea.blaze.base.projectview.section.sections.ImportTargetOutputSection;
 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.ShardBlazeBuildsSection;
 import com.google.idea.blaze.base.projectview.section.sections.TargetSection;
 import com.google.idea.blaze.base.projectview.section.sections.TestSourceSection;
 import com.google.idea.blaze.base.projectview.section.sections.TextBlock;
@@ -83,9 +84,10 @@
                     .add(ListSection.builder(BuildFlagsSection.KEY).add("--android_sdk=abcd"))
                     .add(
                         ListSection.builder(ImportTargetOutputSection.KEY)
-                            .add(new Label("//test:test")))
+                            .add(Label.create("//test:test")))
                     .add(
-                        ListSection.builder(ExcludeTargetSection.KEY).add(new Label("//test:test")))
+                        ListSection.builder(ExcludeTargetSection.KEY)
+                            .add(Label.create("//test:test")))
                     .add(ScalarSection.builder(WorkspaceTypeSection.KEY).set(WorkspaceType.JAVA))
                     .add(
                         ListSection.builder(AdditionalLanguagesSection.KEY).add(LanguageClass.JAVA))
@@ -93,6 +95,7 @@
                     .add(
                         ListSection.builder(RunConfigurationsSection.KEY)
                             .add(new WorkspacePath("test")))
+                    .add(ScalarSection.builder(ShardBlazeBuildsSection.KEY).set(false))
                     .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 fe15957..1b2e31c 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
@@ -15,6 +15,7 @@
  */
 package com.google.idea.blaze.base.projectview;
 
+import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableSet;
 import com.google.common.collect.Sets;
 import com.google.idea.blaze.base.BlazeTestCase;
@@ -35,6 +36,7 @@
 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 com.intellij.openapi.extensions.impl.ExtensionPointImpl;
 import java.io.File;
 import java.util.Set;
 import org.jetbrains.annotations.NotNull;
@@ -60,7 +62,20 @@
   protected void initTest(
       @NotNull Container applicationServices, @NotNull Container projectServices) {
     super.initTest(applicationServices, projectServices);
-    registerExtensionPoint(BlazeSyncPlugin.EP_NAME, BlazeSyncPlugin.class);
+    ExtensionPointImpl<BlazeSyncPlugin> ep =
+        registerExtensionPoint(BlazeSyncPlugin.EP_NAME, BlazeSyncPlugin.class);
+    ep.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);
+          }
+        });
 
     fileAttributeProvider = new MockFileAttributeProvider(workspaceRoot);
     applicationServices.register(FileAttributeProvider.class, fileAttributeProvider);
@@ -86,7 +101,7 @@
             .build();
     fileAttributeProvider.addProjectView(projectViewSet);
     ProjectViewVerifier.verifyProjectView(
-        context, workspacePathResolver, projectViewSet, workspaceLanguageSettings);
+        project, context, workspacePathResolver, projectViewSet, workspaceLanguageSettings);
     errorCollector.assertNoIssues();
   }
 
@@ -108,7 +123,7 @@
             .build();
     fileAttributeProvider.addProjectView(projectViewSet);
     ProjectViewVerifier.verifyProjectView(
-        context, workspacePathResolver, projectViewSet, workspaceLanguageSettings);
+        project, 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");
@@ -132,7 +147,7 @@
             .build();
     fileAttributeProvider.addProjectView(projectViewSet);
     ProjectViewVerifier.verifyProjectView(
-        context, workspacePathResolver, projectViewSet, workspaceLanguageSettings);
+        project, 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");
@@ -157,7 +172,7 @@
             .build();
     fileAttributeProvider.addProjectView(projectViewSet);
     ProjectViewVerifier.verifyProjectView(
-        context, workspacePathResolver, projectViewSet, workspaceLanguageSettings);
+        project, context, workspacePathResolver, projectViewSet, workspaceLanguageSettings);
     errorCollector.assertNoIssues();
   }
 
@@ -175,7 +190,7 @@
                     .build())
             .build();
     ProjectViewVerifier.verifyProjectView(
-        context, workspacePathResolver, projectViewSet, workspaceLanguageSettings);
+        project, context, workspacePathResolver, projectViewSet, workspaceLanguageSettings);
     errorCollector.assertIssues(
         String.format(
             "Directory '%s' specified in project view not found.",
@@ -197,7 +212,7 @@
             .build();
     fileAttributeProvider.addFile(new WorkspacePath("java/com/google/android/apps/example"));
     ProjectViewVerifier.verifyProjectView(
-        context, workspacePathResolver, projectViewSet, workspaceLanguageSettings);
+        project, context, workspacePathResolver, projectViewSet, workspaceLanguageSettings);
     errorCollector.assertIssues(
         String.format(
             "Directory '%s' specified in project view is a file.",
diff --git a/base/tests/unittests/com/google/idea/blaze/base/projectview/parser/ProjectViewParserTest.java b/base/tests/unittests/com/google/idea/blaze/base/projectview/parser/ProjectViewParserTest.java
index 08fea57..52ed8cf 100644
--- a/base/tests/unittests/com/google/idea/blaze/base/projectview/parser/ProjectViewParserTest.java
+++ b/base/tests/unittests/com/google/idea/blaze/base/projectview/parser/ProjectViewParserTest.java
@@ -48,8 +48,8 @@
 import java.io.IOException;
 import java.util.Collection;
 import java.util.Map;
+import javax.annotation.Nullable;
 import org.jetbrains.annotations.NotNull;
-import org.jetbrains.annotations.Nullable;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.junit.runners.JUnit4;
diff --git a/base/tests/unittests/com/google/idea/blaze/base/run/BlazeCommandRunConfigurationTest.java b/base/tests/unittests/com/google/idea/blaze/base/run/BlazeCommandRunConfigurationTest.java
index 2f1cac6..f0047bd 100644
--- a/base/tests/unittests/com/google/idea/blaze/base/run/BlazeCommandRunConfigurationTest.java
+++ b/base/tests/unittests/com/google/idea/blaze/base/run/BlazeCommandRunConfigurationTest.java
@@ -43,7 +43,7 @@
 @RunWith(JUnit4.class)
 public class BlazeCommandRunConfigurationTest extends BlazeTestCase {
   private static final BlazeImportSettings DUMMY_IMPORT_SETTINGS =
-      new BlazeImportSettings("", "", "", "", "", Blaze.BuildSystem.Blaze);
+      new BlazeImportSettings("", "", "", "", Blaze.BuildSystem.Blaze);
 
   private final BlazeCommandRunConfigurationType type = new BlazeCommandRunConfigurationType();
   private BlazeCommandRunConfiguration configuration;
@@ -54,8 +54,7 @@
     super.initTest(applicationServices, projectServices);
 
     applicationServices.register(UISettings.class, new UISettings());
-    projectServices.register(
-        BlazeImportSettingsManager.class, new BlazeImportSettingsManager(project));
+    projectServices.register(BlazeImportSettingsManager.class, new BlazeImportSettingsManager());
     BlazeImportSettingsManager.getInstance(getProject()).setImportSettings(DUMMY_IMPORT_SETTINGS);
 
     applicationServices.register(ExperimentService.class, new MockExperimentService());
@@ -71,7 +70,7 @@
 
   @Test
   public void readAndWriteShouldMatch() throws Exception {
-    Label label = new Label("//package:rule");
+    Label label = Label.create("//package:rule");
     configuration.setTarget(label);
 
     Element element = new Element("test");
diff --git a/base/tests/unittests/com/google/idea/blaze/base/run/RuleNameHeuristicTest.java b/base/tests/unittests/com/google/idea/blaze/base/run/RuleNameHeuristicTest.java
index 2923947..70a85be 100644
--- a/base/tests/unittests/com/google/idea/blaze/base/run/RuleNameHeuristicTest.java
+++ b/base/tests/unittests/com/google/idea/blaze/base/run/RuleNameHeuristicTest.java
@@ -46,7 +46,8 @@
     File source = new File("java/com/foo/FooTest.java");
     TargetIdeInfo target =
         TargetIdeInfo.builder().setLabel("//foo:FooTest").setKind("java_test").build();
-    assertThat(new TargetNameHeuristic().matchesSource(target, source, null)).isTrue();
+    assertThat(new TargetNameHeuristic().matchesSource(project, target, null, source, null))
+        .isTrue();
   }
 
   @Test
@@ -54,7 +55,8 @@
     File source = new File("java/com/foo/FooTest.java");
     TargetIdeInfo target =
         TargetIdeInfo.builder().setLabel("//foo:foo/FooTest").setKind("java_test").build();
-    assertThat(new TargetNameHeuristic().matchesSource(target, source, null)).isTrue();
+    assertThat(new TargetNameHeuristic().matchesSource(project, target, null, source, null))
+        .isTrue();
   }
 
   @Test
@@ -62,7 +64,8 @@
     File source = new File("java/com/foo/BarFooTest.java");
     TargetIdeInfo target =
         TargetIdeInfo.builder().setLabel("//foo:FooTest").setKind("java_test").build();
-    assertThat(new TargetNameHeuristic().matchesSource(target, source, null)).isFalse();
+    assertThat(new TargetNameHeuristic().matchesSource(project, target, null, source, null))
+        .isFalse();
   }
 
   @Test
@@ -70,7 +73,8 @@
     File source = new File("java/com/foo/FooTest.java");
     TargetIdeInfo target =
         TargetIdeInfo.builder().setLabel("//foo:bar/FooTest").setKind("java_test").build();
-    assertThat(new TargetNameHeuristic().matchesSource(target, source, null)).isFalse();
+    assertThat(new TargetNameHeuristic().matchesSource(project, target, null, source, null))
+        .isFalse();
   }
 
   @Test
@@ -78,7 +82,8 @@
     File source = new File("java/com/foo/FooTest.java");
     TargetIdeInfo target =
         TargetIdeInfo.builder().setLabel("//foo:ForTest").setKind("java_test").build();
-    assertThat(new TargetNameHeuristic().matchesSource(target, source, null)).isFalse();
+    assertThat(new TargetNameHeuristic().matchesSource(project, target, null, source, null))
+        .isFalse();
   }
 
   @Test
@@ -88,8 +93,9 @@
         ImmutableList.of(
             TargetIdeInfo.builder().setLabel("//foo:FirstTest").setKind("java_test").build(),
             TargetIdeInfo.builder().setLabel("//bar:OtherTest").setKind("java_test").build());
-    Label match = TestTargetHeuristic.chooseTestTargetForSourceFile(source, targets, null);
-    assertThat(match).isEqualTo(new Label("//foo:FirstTest"));
+    Label match =
+        TestTargetHeuristic.chooseTestTargetForSourceFile(project, null, source, targets, null);
+    assertThat(match).isEqualTo(Label.create("//foo:FirstTest"));
   }
 
   @Test
@@ -99,8 +105,9 @@
         ImmutableList.of(
             TargetIdeInfo.builder().setLabel("//bar:FirstTest").setKind("java_test").build(),
             TargetIdeInfo.builder().setLabel("//foo:FooTest").setKind("java_test").build());
-    Label match = TestTargetHeuristic.chooseTestTargetForSourceFile(source, targets, null);
-    assertThat(match).isEqualTo(new Label("//foo:FooTest"));
+    Label match =
+        TestTargetHeuristic.chooseTestTargetForSourceFile(project, null, source, targets, null);
+    assertThat(match).isEqualTo(Label.create("//foo:FooTest"));
   }
 
   @Test
@@ -111,7 +118,8 @@
             TargetIdeInfo.builder().setLabel("//bar:OtherTest").setKind("java_test").build(),
             TargetIdeInfo.builder().setLabel("//foo:FooTest").setKind("java_test").build(),
             TargetIdeInfo.builder().setLabel("//bar/foo:FooTest").setKind("java_test").build());
-    Label match = TestTargetHeuristic.chooseTestTargetForSourceFile(source, targets, null);
-    assertThat(match).isEqualTo(new Label("//foo:FooTest"));
+    Label match =
+        TestTargetHeuristic.chooseTestTargetForSourceFile(project, null, source, targets, null);
+    assertThat(match).isEqualTo(Label.create("//foo:FooTest"));
   }
 }
diff --git a/base/tests/unittests/com/google/idea/blaze/base/run/TestSizeHeuristicTest.java b/base/tests/unittests/com/google/idea/blaze/base/run/TestSizeHeuristicTest.java
index 99ce084..9df7417 100644
--- a/base/tests/unittests/com/google/idea/blaze/base/run/TestSizeHeuristicTest.java
+++ b/base/tests/unittests/com/google/idea/blaze/base/run/TestSizeHeuristicTest.java
@@ -51,7 +51,9 @@
             .setKind("java_test")
             .setTestInfo(TestIdeInfo.builder().setTestSize(TestSize.MEDIUM))
             .build();
-    assertThat(new TestSizeHeuristic().matchesSource(target, source, TestSize.MEDIUM)).isTrue();
+    assertThat(
+            new TestSizeHeuristic().matchesSource(project, target, null, source, TestSize.MEDIUM))
+        .isTrue();
   }
 
   @Test
@@ -63,7 +65,8 @@
             .setKind("java_test")
             .setTestInfo(TestIdeInfo.builder().setTestSize(TestSize.MEDIUM))
             .build();
-    assertThat(new TestSizeHeuristic().matchesSource(target, source, TestSize.SMALL)).isFalse();
+    assertThat(new TestSizeHeuristic().matchesSource(project, target, null, source, TestSize.SMALL))
+        .isFalse();
   }
 
   @Test
@@ -75,7 +78,7 @@
             .setKind("java_test")
             .setTestInfo(TestIdeInfo.builder().setTestSize(TestSize.SMALL))
             .build();
-    assertThat(new TestSizeHeuristic().matchesSource(target, source, null)).isTrue();
+    assertThat(new TestSizeHeuristic().matchesSource(project, target, null, source, null)).isTrue();
 
     target =
         TargetIdeInfo.builder()
@@ -83,7 +86,8 @@
             .setKind("java_test")
             .setTestInfo(TestIdeInfo.builder().setTestSize(TestSize.MEDIUM))
             .build();
-    assertThat(new TestSizeHeuristic().matchesSource(target, source, null)).isFalse();
+    assertThat(new TestSizeHeuristic().matchesSource(project, target, null, source, null))
+        .isFalse();
   }
 
   @Test
@@ -106,8 +110,10 @@
                 .setKind("java_test")
                 .setTestInfo(TestIdeInfo.builder().setTestSize(TestSize.ENORMOUS))
                 .build());
-    Label match = TestTargetHeuristic.chooseTestTargetForSourceFile(source, rules, TestSize.SMALL);
-    assertThat(match).isEqualTo(new Label("//foo:test1"));
+    Label match =
+        TestTargetHeuristic.chooseTestTargetForSourceFile(
+            project, null, source, rules, TestSize.SMALL);
+    assertThat(match).isEqualTo(Label.create("//foo:test1"));
   }
 
   @Test
@@ -125,8 +131,10 @@
                 .setKind("java_test")
                 .setTestInfo(TestIdeInfo.builder().setTestSize(TestSize.SMALL))
                 .build());
-    Label match = TestTargetHeuristic.chooseTestTargetForSourceFile(source, rules, TestSize.SMALL);
-    assertThat(match).isEqualTo(new Label("//foo:test2"));
+    Label match =
+        TestTargetHeuristic.chooseTestTargetForSourceFile(
+            project, null, source, rules, TestSize.SMALL);
+    assertThat(match).isEqualTo(Label.create("//foo:test2"));
   }
 
   @Test
@@ -149,7 +157,9 @@
                 .setKind("java_test")
                 .setTestInfo(TestIdeInfo.builder().setTestSize(TestSize.SMALL))
                 .build());
-    Label match = TestTargetHeuristic.chooseTestTargetForSourceFile(source, rules, TestSize.SMALL);
-    assertThat(match).isEqualTo(new Label("//foo:test2"));
+    Label match =
+        TestTargetHeuristic.chooseTestTargetForSourceFile(
+            project, null, source, rules, TestSize.SMALL);
+    assertThat(match).isEqualTo(Label.create("//foo:test2"));
   }
 }
diff --git a/base/tests/unittests/com/google/idea/blaze/base/run/TestTargetSourcesHeuristicTest.java b/base/tests/unittests/com/google/idea/blaze/base/run/TestTargetSourcesHeuristicTest.java
new file mode 100644
index 0000000..0663264
--- /dev/null
+++ b/base/tests/unittests/com/google/idea/blaze/base/run/TestTargetSourcesHeuristicTest.java
@@ -0,0 +1,131 @@
+/*
+ * Copyright 2017 The Bazel Authors. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.idea.blaze.base.run;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.google.common.collect.ImmutableList;
+import com.google.idea.blaze.base.BlazeTestCase;
+import com.google.idea.blaze.base.ideinfo.ArtifactLocation;
+import com.google.idea.blaze.base.ideinfo.TargetIdeInfo;
+import com.google.idea.blaze.base.model.BlazeProjectData;
+import com.google.idea.blaze.base.model.MockBlazeProjectDataBuilder;
+import com.google.idea.blaze.base.model.MockBlazeProjectDataManager;
+import com.google.idea.blaze.base.model.primitives.Label;
+import com.google.idea.blaze.base.model.primitives.WorkspacePath;
+import com.google.idea.blaze.base.model.primitives.WorkspaceRoot;
+import com.google.idea.blaze.base.sync.data.BlazeProjectDataManager;
+import com.intellij.openapi.extensions.impl.ExtensionPointImpl;
+import java.io.File;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** Unit tests for {@link TestTargetSourcesHeuristic}. */
+@RunWith(JUnit4.class)
+public class TestTargetSourcesHeuristicTest extends BlazeTestCase {
+
+  private final WorkspaceRoot workspaceRoot = new WorkspaceRoot(new File("/"));
+
+  @Override
+  protected void initTest(Container applicationServices, Container projectServices) {
+    super.initTest(applicationServices, projectServices);
+
+    BlazeProjectData blazeProjectData = MockBlazeProjectDataBuilder.builder(workspaceRoot).build();
+    projectServices.register(
+        BlazeProjectDataManager.class, new MockBlazeProjectDataManager(blazeProjectData));
+
+    ExtensionPointImpl<TestTargetHeuristic> ep =
+        registerExtensionPoint(TestTargetHeuristic.EP_NAME, TestTargetHeuristic.class);
+    ep.registerExtension(new TestTargetSourcesHeuristic());
+  }
+
+  @Test
+  public void testPredicateNoSources() {
+    File source = workspaceRoot.fileForPath(new WorkspacePath("java/com/foo/FooTest.java"));
+    TargetIdeInfo target =
+        TargetIdeInfo.builder().setLabel("//foo:test").setKind("java_test").build();
+    assertThat(new TestTargetSourcesHeuristic().matchesSource(project, target, null, source, null))
+        .isFalse();
+  }
+
+  @Test
+  public void testPredicateNoMatchingSource() {
+    File source = workspaceRoot.fileForPath(new WorkspacePath("java/com/foo/FooTest.java"));
+    TargetIdeInfo target =
+        TargetIdeInfo.builder()
+            .setLabel("//foo:test")
+            .setKind("java_test")
+            .addSource(sourceRoot("java/com/bar/OtherTest.java"))
+            .build();
+    assertThat(new TestTargetSourcesHeuristic().matchesSource(project, target, null, source, null))
+        .isFalse();
+  }
+
+  @Test
+  public void testPredicateMatchingSource() {
+    File source = workspaceRoot.fileForPath(new WorkspacePath("java/com/foo/FooTest.java"));
+    TargetIdeInfo target =
+        TargetIdeInfo.builder()
+            .setLabel("//foo:test")
+            .setKind("java_test")
+            .addSource(sourceRoot("java/com/bar/OtherTest.java"))
+            .addSource(sourceRoot("java/com/foo/FooTest.java"))
+            .build();
+    assertThat(new TestTargetSourcesHeuristic().matchesSource(project, target, null, source, null))
+        .isTrue();
+  }
+
+  @Test
+  public void testFilterNoMatchesFallBackToFirstRule() throws Exception {
+    File source = workspaceRoot.fileForPath(new WorkspacePath("java/com/foo/FooTest.java"));
+    ImmutableList<TargetIdeInfo> rules =
+        ImmutableList.of(
+            TargetIdeInfo.builder()
+                .setLabel("//foo:test1")
+                .setKind("java_test")
+                .addSource(sourceRoot("java/com/bar/OtherTest.java"))
+                .build(),
+            TargetIdeInfo.builder().setLabel("//foo:test2").setKind("java_test").build());
+    Label match =
+        TestTargetHeuristic.chooseTestTargetForSourceFile(project, null, source, rules, null);
+    assertThat(match).isEqualTo(Label.create("//foo:test1"));
+  }
+
+  @Test
+  public void testFilterOneMatch() throws Exception {
+    File source = workspaceRoot.fileForPath(new WorkspacePath("java/com/foo/FooTest.java"));
+    ImmutableList<TargetIdeInfo> rules =
+        ImmutableList.of(
+            TargetIdeInfo.builder()
+                .setLabel("//foo:test1")
+                .setKind("java_test")
+                .addSource(sourceRoot("java/com/bar/OtherTest.java"))
+                .build(),
+            TargetIdeInfo.builder()
+                .setLabel("//foo:test2")
+                .setKind("java_test")
+                .addSource(sourceRoot("java/com/foo/FooTest.java"))
+                .build());
+    Label match =
+        TestTargetHeuristic.chooseTestTargetForSourceFile(project, null, source, rules, null);
+    assertThat(match).isEqualTo(Label.create("//foo:test2"));
+  }
+
+  private static ArtifactLocation sourceRoot(String relativePath) {
+    return ArtifactLocation.builder().setRelativePath(relativePath).setIsSource(true).build();
+  }
+}
diff --git a/base/tests/unittests/com/google/idea/blaze/base/run/filter/BlazeTargetFilterTest.java b/base/tests/unittests/com/google/idea/blaze/base/run/filter/BlazeTargetFilterTest.java
new file mode 100644
index 0000000..4563886
--- /dev/null
+++ b/base/tests/unittests/com/google/idea/blaze/base/run/filter/BlazeTargetFilterTest.java
@@ -0,0 +1,61 @@
+/*
+ * Copyright 2017 The Bazel Authors. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.idea.blaze.base.run.filter;
+
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.idea.blaze.base.run.filter.BlazeTargetFilter.TARGET_PATTERN;
+
+import java.util.regex.Matcher;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** Unit tests for {@link BlazeTargetFilter}. */
+@RunWith(JUnit4.class)
+public class BlazeTargetFilterTest {
+
+  @Test
+  public void testSimpleTarget() {
+    String line = "Something //package:target_name something else";
+    Matcher matcher = TARGET_PATTERN.matcher(line);
+    assertThat(matcher.find()).isTrue();
+    assertThat(matcher.group()).isEqualTo("//package:target_name");
+  }
+
+  @Test
+  public void testExternalWorkspaceTarget() {
+    String line = "Something @ext//package:target_name something else";
+    Matcher matcher = TARGET_PATTERN.matcher(line);
+    assertThat(matcher.find()).isTrue();
+    assertThat(matcher.group()).isEqualTo("@ext//package:target_name");
+  }
+
+  @Test
+  public void testQuotedTarget() {
+    String line = "Something '//package:target_name' something else";
+    Matcher matcher = TARGET_PATTERN.matcher(line);
+    assertThat(matcher.find()).isTrue();
+    assertThat(matcher.group()).isEqualTo("//package:target_name");
+  }
+
+  @Test
+  public void testUnusualCharsInTarget() {
+    String line = "Something //Package-._$():T0+,=~#target_@name something else";
+    Matcher matcher = TARGET_PATTERN.matcher(line);
+    assertThat(matcher.find()).isTrue();
+    assertThat(matcher.group()).isEqualTo("//Package-._$():T0+,=~#target_@name");
+  }
+}
diff --git a/base/tests/unittests/com/google/idea/blaze/base/run/smrunner/BlazeXmlSchemaTest.java b/base/tests/unittests/com/google/idea/blaze/base/run/smrunner/BlazeXmlSchemaTest.java
index 5c7dc35..2ed69f5 100644
--- a/base/tests/unittests/com/google/idea/blaze/base/run/smrunner/BlazeXmlSchemaTest.java
+++ b/base/tests/unittests/com/google/idea/blaze/base/run/smrunner/BlazeXmlSchemaTest.java
@@ -24,6 +24,7 @@
 import java.io.InputStream;
 import java.nio.charset.StandardCharsets;
 import java.util.List;
+import java.util.stream.Collectors;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.junit.runners.JUnit4;
@@ -68,4 +69,51 @@
     TestSuite parsed = BlazeXmlSchema.parse(stream);
     assertThat(parsed).isNotNull();
   }
+
+  @Test
+  public void testMergeShardedTests() {
+    TestSuite shard1 =
+        parseXml(
+            "<?xml version='1.0' encoding='UTF-8'?>",
+            "<testsuites>",
+            "  <testsuite name='com.google.ConfigTest' time='10' tests='2' failures='1'>",
+            "    <testcase name='testCase1' time='2.1' status='run' result='completed'/>",
+            "    <testcase name='testCase2' time='7.9' status='run' result='completed'>",
+            "      <failure message='failed'/>",
+            "    </testcase>",
+            "  </testsuite>",
+            "</testsuites>");
+    TestSuite shard2 =
+        parseXml(
+            "<?xml version='1.0' encoding='UTF-8'?>",
+            "<testsuites>",
+            "  <testsuite name='com.google.ConfigTest' time='5' tests='2' failures='1'>",
+            "    <testcase name='testCase3' time='1' status='run' result='completed'/>",
+            "    <testcase name='testCase4' time='4' status='run' result='completed'>",
+            "      <failure message='failed'/>",
+            "    </testcase>",
+            "  </testsuite>",
+            "</testsuites>");
+    TestSuite mergedOuter = BlazeXmlSchema.mergeSuites(ImmutableList.of(shard1, shard2));
+    assertThat(mergedOuter.testSuites).hasSize(1);
+    TestSuite mergedInner = mergedOuter.testSuites.get(0).testSuites.get(0);
+    assertThat(mergedInner.name).isEqualTo("com.google.ConfigTest");
+    assertThat(mergedInner.time).isEqualTo(15d);
+    assertThat(mergedInner.tests).isEqualTo(4);
+    assertThat(mergedInner.failures).isEqualTo(2);
+    assertThat(mergedInner.testCases).hasSize(4);
+    assertThat(
+            mergedInner
+                .testCases
+                .stream()
+                .map(testCase -> testCase.name)
+                .collect(Collectors.toList()))
+        .containsExactly("testCase1", "testCase2", "testCase3", "testCase4");
+  }
+
+  private static TestSuite parseXml(String... lines) {
+    InputStream stream =
+        new ByteArrayInputStream(Joiner.on('\n').join(lines).getBytes(StandardCharsets.UTF_8));
+    return BlazeXmlSchema.parse(stream);
+  }
 }
diff --git a/base/tests/unittests/com/google/idea/blaze/base/run/state/BlazeCommandRunConfigurationCommonStateTest.java b/base/tests/unittests/com/google/idea/blaze/base/run/state/BlazeCommandRunConfigurationCommonStateTest.java
index 5598b49..5aa8521 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
@@ -37,7 +37,7 @@
 @RunWith(JUnit4.class)
 public class BlazeCommandRunConfigurationCommonStateTest extends BlazeTestCase {
   private static final BlazeImportSettings DUMMY_IMPORT_SETTINGS =
-      new BlazeImportSettings("", "", "", "", "", Blaze.BuildSystem.Blaze);
+      new BlazeImportSettings("", "", "", "", Blaze.BuildSystem.Blaze);
   private static final BlazeCommandName COMMAND = BlazeCommandName.fromString("command");
 
   private BlazeCommandRunConfigurationCommonState state;
@@ -47,8 +47,7 @@
     super.initTest(applicationServices, projectServices);
 
     applicationServices.register(UISettings.class, new UISettings());
-    projectServices.register(
-        BlazeImportSettingsManager.class, new BlazeImportSettingsManager(project));
+    projectServices.register(BlazeImportSettingsManager.class, new BlazeImportSettingsManager());
     BlazeImportSettingsManager.getInstance(getProject()).setImportSettings(DUMMY_IMPORT_SETTINGS);
 
     registerExtensionPoint(DistributedExecutorSupport.EP_NAME, DistributedExecutorSupport.class);
@@ -58,10 +57,10 @@
 
   @Test
   public void readAndWriteShouldMatch() throws Exception {
-    state.setCommand(COMMAND);
-    state.setBlazeFlags(ImmutableList.of("--flag1", "--flag2"));
-    state.setExeFlags(ImmutableList.of("--exeFlag1"));
-    state.setBlazeBinary("/usr/bin/blaze");
+    state.getCommandState().setCommand(COMMAND);
+    state.getBlazeFlagsState().setRawFlags(ImmutableList.of("--flag1", "--flag2"));
+    state.getExeFlagsState().setRawFlags(ImmutableList.of("--exeFlag1"));
+    state.getBlazeBinaryState().setBlazeBinary("/usr/bin/blaze");
 
     Element element = new Element("test");
     state.writeExternal(element);
@@ -69,10 +68,12 @@
         new BlazeCommandRunConfigurationCommonState(Blaze.getBuildSystem(project));
     readState.readExternal(element);
 
-    assertThat(readState.getCommand()).isEqualTo(COMMAND);
-    assertThat(readState.getBlazeFlags()).containsExactly("--flag1", "--flag2").inOrder();
-    assertThat(readState.getExeFlags()).containsExactly("--exeFlag1");
-    assertThat(readState.getBlazeBinary()).isEqualTo("/usr/bin/blaze");
+    assertThat(readState.getCommandState().getCommand()).isEqualTo(COMMAND);
+    assertThat(readState.getBlazeFlagsState().getRawFlags())
+        .containsExactly("--flag1", "--flag2")
+        .inOrder();
+    assertThat(readState.getExeFlagsState().getRawFlags()).containsExactly("--exeFlag1");
+    assertThat(readState.getBlazeBinaryState().getBlazeBinary()).isEqualTo("/usr/bin/blaze");
   }
 
   @Test
@@ -83,16 +84,24 @@
         new BlazeCommandRunConfigurationCommonState(Blaze.getBuildSystem(project));
     readState.readExternal(element);
 
-    assertThat(readState.getCommand()).isEqualTo(state.getCommand());
-    assertThat(readState.getBlazeFlags()).isEqualTo(state.getBlazeFlags());
-    assertThat(readState.getExeFlags()).isEqualTo(state.getExeFlags());
-    assertThat(readState.getBlazeBinary()).isEqualTo(state.getBlazeBinary());
+    assertThat(readState.getCommandState().getCommand())
+        .isEqualTo(state.getCommandState().getCommand());
+    assertThat(readState.getBlazeFlagsState().getRawFlags())
+        .isEqualTo(state.getBlazeFlagsState().getRawFlags());
+    assertThat(readState.getExeFlagsState().getRawFlags())
+        .isEqualTo(state.getExeFlagsState().getRawFlags());
+    assertThat(readState.getBlazeBinaryState().getBlazeBinary())
+        .isEqualTo(state.getBlazeBinaryState().getBlazeBinary());
   }
 
   @Test
   public void readShouldOmitEmptyFlags() throws Exception {
-    state.setBlazeFlags(Lists.newArrayList("hi ", "", "I'm", " ", "\t", "Josh\r\n", "\n"));
-    state.setExeFlags(Lists.newArrayList("hi ", "", "I'm", " ", "\t", "Josh\r\n", "\n"));
+    state
+        .getBlazeFlagsState()
+        .setRawFlags(Lists.newArrayList("hi ", "", "I'm", " ", "\t", "Josh\r\n", "\n"));
+    state
+        .getExeFlagsState()
+        .setRawFlags(Lists.newArrayList("hi ", "", "I'm", " ", "\t", "Josh\r\n", "\n"));
 
     Element element = new Element("test");
     state.writeExternal(element);
@@ -100,18 +109,22 @@
         new BlazeCommandRunConfigurationCommonState(Blaze.getBuildSystem(project));
     readState.readExternal(element);
 
-    assertThat(readState.getBlazeFlags()).containsExactly("hi", "I'm", "Josh").inOrder();
-    assertThat(readState.getExeFlags()).containsExactly("hi", "I'm", "Josh").inOrder();
+    assertThat(readState.getBlazeFlagsState().getRawFlags())
+        .containsExactly("hi", "I'm", "Josh")
+        .inOrder();
+    assertThat(readState.getExeFlagsState().getRawFlags())
+        .containsExactly("hi", "I'm", "Josh")
+        .inOrder();
   }
 
   @Test
   public void repeatedWriteShouldNotChangeElement() throws Exception {
     final XMLOutputter xmlOutputter = new XMLOutputter(Format.getCompactFormat());
 
-    state.setCommand(COMMAND);
-    state.setBlazeFlags(ImmutableList.of("--flag1", "--flag2"));
-    state.setExeFlags(ImmutableList.of("--exeFlag1"));
-    state.setBlazeBinary("/usr/bin/blaze");
+    state.getCommandState().setCommand(COMMAND);
+    state.getBlazeFlagsState().setRawFlags(ImmutableList.of("--flag1", "--flag2"));
+    state.getExeFlagsState().setRawFlags(ImmutableList.of("--exeFlag1"));
+    state.getBlazeBinaryState().setBlazeBinary("/usr/bin/blaze");
 
     Element firstWrite = new Element("test");
     state.writeExternal(firstWrite);
@@ -126,20 +139,24 @@
   public void editorApplyToAndResetFromShouldMatch() throws Exception {
     RunConfigurationStateEditor editor = state.getEditor(project);
 
-    state.setCommand(COMMAND);
-    state.setBlazeFlags(ImmutableList.of("--flag1", "--flag2"));
-    state.setExeFlags(ImmutableList.of("--exeFlag1", "--exeFlag2"));
-    state.setBlazeBinary("/usr/bin/blaze");
+    state.getCommandState().setCommand(COMMAND);
+    state.getBlazeFlagsState().setRawFlags(ImmutableList.of("--flag1", "--flag2"));
+    state.getExeFlagsState().setRawFlags(ImmutableList.of("--exeFlag1", "--exeFlag2"));
+    state.getBlazeBinaryState().setBlazeBinary("/usr/bin/blaze");
 
     editor.resetEditorFrom(state);
     BlazeCommandRunConfigurationCommonState readState =
         new BlazeCommandRunConfigurationCommonState(Blaze.getBuildSystem(project));
     editor.applyEditorTo(readState);
 
-    assertThat(readState.getCommand()).isEqualTo(state.getCommand());
-    assertThat(readState.getBlazeFlags()).isEqualTo(state.getBlazeFlags());
-    assertThat(readState.getExeFlags()).isEqualTo(state.getExeFlags());
-    assertThat(readState.getBlazeBinary()).isEqualTo(state.getBlazeBinary());
+    assertThat(readState.getCommandState().getCommand())
+        .isEqualTo(state.getCommandState().getCommand());
+    assertThat(readState.getBlazeFlagsState().getRawFlags())
+        .isEqualTo(state.getBlazeFlagsState().getRawFlags());
+    assertThat(readState.getExeFlagsState().getRawFlags())
+        .isEqualTo(state.getExeFlagsState().getRawFlags());
+    assertThat(readState.getBlazeBinaryState().getBlazeBinary())
+        .isEqualTo(state.getBlazeBinaryState().getBlazeBinary());
   }
 
   @Test
@@ -151,9 +168,13 @@
         new BlazeCommandRunConfigurationCommonState(Blaze.getBuildSystem(project));
     editor.applyEditorTo(readState);
 
-    assertThat(readState.getCommand()).isEqualTo(state.getCommand());
-    assertThat(readState.getBlazeFlags()).isEqualTo(state.getBlazeFlags());
-    assertThat(readState.getExeFlags()).isEqualTo(state.getExeFlags());
-    assertThat(readState.getBlazeBinary()).isEqualTo(state.getBlazeBinary());
+    assertThat(readState.getCommandState().getCommand())
+        .isEqualTo(state.getCommandState().getCommand());
+    assertThat(readState.getBlazeFlagsState().getRawFlags())
+        .isEqualTo(state.getBlazeFlagsState().getRawFlags());
+    assertThat(readState.getExeFlagsState().getRawFlags())
+        .isEqualTo(state.getExeFlagsState().getRawFlags());
+    assertThat(readState.getBlazeBinaryState().getBlazeBinary())
+        .isEqualTo(state.getBlazeBinaryState().getBlazeBinary());
   }
 }
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
index 730ac3b..ec3daeb 100644
--- 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
@@ -32,26 +32,26 @@
     // 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);
+    state.setRawFlags(flags);
 
     RunConfigurationStateEditor editor = state.getEditor(null);
     editor.resetEditorFrom(state);
     editor.applyEditorTo(state);
 
-    assertThat(state.getFlags()).isEqualTo(flags);
+    assertThat(state.getRawFlags()).isEqualTo(flags);
   }
 
   @Test
   public void testQuotesRetainedAfterReserialization() {
     ImmutableList<String> flags = ImmutableList.of("\"--flag=test\"");
     RunConfigurationFlagsState state = new RunConfigurationFlagsState("tag", "field");
-    state.setFlags(flags);
+    state.setRawFlags(flags);
 
     RunConfigurationStateEditor editor = state.getEditor(null);
     editor.resetEditorFrom(state);
     editor.applyEditorTo(state);
 
-    assertThat(state.getFlags()).isEqualTo(flags);
+    assertThat(state.getRawFlags()).isEqualTo(flags);
   }
 
   @Test
@@ -64,12 +64,12 @@
             "--test_filter=com.google.idea.blaze.base.run.state.RunConfigurationFlagStateTest#",
             "--define=ij_product=intellij-latest");
     RunConfigurationFlagsState state = new RunConfigurationFlagsState("tag", "field");
-    state.setFlags(flags);
+    state.setRawFlags(flags);
 
     RunConfigurationStateEditor editor = state.getEditor(null);
     editor.resetEditorFrom(state);
     editor.applyEditorTo(state);
 
-    assertThat(state.getFlags()).isEqualTo(flags);
+    assertThat(state.getRawFlags()).isEqualTo(flags);
   }
 }
diff --git a/base/tests/unittests/com/google/idea/blaze/base/run/testlogs/BlazeCommandLogParserTest.java b/base/tests/unittests/com/google/idea/blaze/base/run/testlogs/BlazeCommandLogParserTest.java
index e3e63ad..cc7ca2c 100644
--- a/base/tests/unittests/com/google/idea/blaze/base/run/testlogs/BlazeCommandLogParserTest.java
+++ b/base/tests/unittests/com/google/idea/blaze/base/run/testlogs/BlazeCommandLogParserTest.java
@@ -31,13 +31,23 @@
   @Test
   public void testParseTestXmlLine() {
     assertThat(BlazeCommandLogParser.parseTestTarget("//path/to:target    PASSED in 5.3s"))
-        .isEqualTo(new Label("//path/to:target"));
+        .isEqualTo(Label.create("//path/to:target"));
 
     assertThat(BlazeCommandLogParser.parseTestTarget("//path/to:target    FAILED in 5.3s"))
-        .isEqualTo(new Label("//path/to:target"));
+        .isEqualTo(Label.create("//path/to:target"));
 
     assertThat(BlazeCommandLogParser.parseTestTarget("//path/to:target (cached) PASSED in 5.3s"))
-        .isEqualTo(new Label("//path/to:target"));
+        .isEqualTo(Label.create("//path/to:target"));
+  }
+
+  @Test
+  public void testParseFailedTarget() {
+    assertThat(BlazeCommandLogParser.parseBuildFailure("Target //path/to:target failed to build"))
+        .isEqualTo(Label.create("//path/to:target"));
+    assertThat(BlazeCommandLogParser.parseTestTarget("Target //path/to:target failed to build"))
+        .isNull();
+    assertThat(BlazeCommandLogParser.parseBuildFailure("//path/to:target    FAILED in 5.3s"))
+        .isNull();
   }
 
   @Test
@@ -62,8 +72,8 @@
             "Executed 1 out of 3 test: 2 test passes.");
     assertThat(BlazeCommandLogParser.parseTestTargets(lines.stream()))
         .containsExactly(
-            new Label("//base:integration_tests"),
-            new Label("//base:unit_tests"),
-            new Label("//golang:unit_tests"));
+            Label.create("//base:integration_tests"),
+            Label.create("//base:unit_tests"),
+            Label.create("//golang:unit_tests"));
   }
 }
diff --git a/base/tests/unittests/com/google/idea/blaze/base/run/testmap/TestMapTest.java b/base/tests/unittests/com/google/idea/blaze/base/run/testmap/TestMapTest.java
index 8a855ae..a43534a 100644
--- a/base/tests/unittests/com/google/idea/blaze/base/run/testmap/TestMapTest.java
+++ b/base/tests/unittests/com/google/idea/blaze/base/run/testmap/TestMapTest.java
@@ -69,7 +69,7 @@
     ImmutableMultimap<TargetKey, TargetKey> reverseDependencies =
         ReverseDependencyMap.createRdepsMap(targetMap);
     assertThat(testMap.targetsForSourceFile(reverseDependencies, new File("/test/Test.java")))
-        .containsExactly(new Label("//test:test"));
+        .containsExactly(Label.create("//test:test"));
   }
 
   @Test
@@ -95,7 +95,7 @@
     ImmutableMultimap<TargetKey, TargetKey> reverseDependencies =
         ReverseDependencyMap.createRdepsMap(targetMap);
     assertThat(testMap.targetsForSourceFile(reverseDependencies, new File("/test/Test.java")))
-        .containsExactly(new Label("//test:test"));
+        .containsExactly(Label.create("//test:test"));
   }
 
   @Test
@@ -127,7 +127,7 @@
     ImmutableMultimap<TargetKey, TargetKey> reverseDependencies =
         ReverseDependencyMap.createRdepsMap(targetMap);
     assertThat(testMap.targetsForSourceFile(reverseDependencies, new File("/test/Test.java")))
-        .containsExactly(new Label("//test:test"), new Label("//test:test2"));
+        .containsExactly(Label.create("//test:test"), Label.create("//test:test2"));
   }
 
   @Test
@@ -165,7 +165,7 @@
     ImmutableMultimap<TargetKey, TargetKey> reverseDependencies =
         ReverseDependencyMap.createRdepsMap(targetMap);
     assertThat(testMap.targetsForSourceFile(reverseDependencies, new File("/test/Test.java")))
-        .containsExactly(new Label("//test:test"), new Label("//test:test2"))
+        .containsExactly(Label.create("//test:test"), Label.create("//test:test2"))
         .inOrder();
   }
 
@@ -204,7 +204,7 @@
     ImmutableMultimap<TargetKey, TargetKey> reverseDependencies =
         ReverseDependencyMap.createRdepsMap(targetMap);
     assertThat(testMap.targetsForSourceFile(reverseDependencies, new File("/test/Test.java")))
-        .containsExactly(new Label("//test:test"), new Label("//test:test2"));
+        .containsExactly(Label.create("//test:test"), Label.create("//test:test2"));
   }
 
   @Test
@@ -237,7 +237,7 @@
     ImmutableMultimap<TargetKey, TargetKey> reverseDependencies =
         ReverseDependencyMap.createRdepsMap(targetMap);
     assertThat(testMap.targetsForSourceFile(reverseDependencies, new File("/test/Test.java")))
-        .containsExactly(new Label("//test:test"));
+        .containsExactly(Label.create("//test:test"));
   }
 
   private ArtifactLocation sourceRoot(String relativePath) {
diff --git a/base/tests/unittests/com/google/idea/blaze/base/sync/LanguageSupportTest.java b/base/tests/unittests/com/google/idea/blaze/base/sync/LanguageSupportTest.java
index af61e7f..fdc880e 100644
--- a/base/tests/unittests/com/google/idea/blaze/base/sync/LanguageSupportTest.java
+++ b/base/tests/unittests/com/google/idea/blaze/base/sync/LanguageSupportTest.java
@@ -81,7 +81,7 @@
                     .build())
             .build();
     WorkspaceLanguageSettings workspaceLanguageSettings =
-        LanguageSupport.createWorkspaceLanguageSettings(context, projectViewSet);
+        LanguageSupport.createWorkspaceLanguageSettings(projectViewSet);
     errorCollector.assertNoIssues();
     assertThat(workspaceLanguageSettings)
         .isEqualTo(
@@ -98,7 +98,9 @@
                     .add(ScalarSection.builder(WorkspaceTypeSection.KEY).set(WorkspaceType.C))
                     .build())
             .build();
-    LanguageSupport.createWorkspaceLanguageSettings(context, projectViewSet);
+    WorkspaceLanguageSettings settings =
+        LanguageSupport.createWorkspaceLanguageSettings(projectViewSet);
+    LanguageSupport.validateLanguageSettings(context, settings);
     errorCollector.assertIssues("Workspace type 'c' is not supported by this plugin");
   }
 
@@ -127,7 +129,9 @@
                             .add(LanguageClass.PYTHON))
                     .build())
             .build();
-    LanguageSupport.createWorkspaceLanguageSettings(context, projectViewSet);
+    WorkspaceLanguageSettings settings =
+        LanguageSupport.createWorkspaceLanguageSettings(projectViewSet);
+    LanguageSupport.validateLanguageSettings(context, settings);
     errorCollector.assertIssues("Language 'python' is not supported by this plugin");
   }
 
@@ -155,7 +159,7 @@
                     .build())
             .build();
     WorkspaceLanguageSettings workspaceLanguageSettings =
-        LanguageSupport.createWorkspaceLanguageSettings(context, projectViewSet);
+        LanguageSupport.createWorkspaceLanguageSettings(projectViewSet);
     assertThat(workspaceLanguageSettings)
         .isEqualTo(
             new WorkspaceLanguageSettings(
diff --git a/base/tests/unittests/com/google/idea/blaze/base/sync/aspects/BlazeIdeInterfaceAspectsImplTest.java b/base/tests/unittests/com/google/idea/blaze/base/sync/aspects/BlazeIdeInterfaceAspectsImplTest.java
index b064721..249783c 100644
--- a/base/tests/unittests/com/google/idea/blaze/base/sync/aspects/BlazeIdeInterfaceAspectsImplTest.java
+++ b/base/tests/unittests/com/google/idea/blaze/base/sync/aspects/BlazeIdeInterfaceAspectsImplTest.java
@@ -16,16 +16,12 @@
 package com.google.idea.blaze.base.sync.aspects;
 
 import com.google.common.collect.ImmutableMap;
-import com.google.common.collect.ImmutableSet;
 import com.google.idea.blaze.base.BlazeTestCase;
 import com.google.idea.blaze.base.TestUtils;
 import com.google.idea.blaze.base.ideinfo.TargetIdeInfo;
 import com.google.idea.blaze.base.ideinfo.TargetMap;
 import com.google.idea.blaze.base.io.FileAttributeProvider;
 import com.google.idea.blaze.base.model.primitives.Label;
-import com.google.idea.blaze.base.model.primitives.LanguageClass;
-import com.google.idea.blaze.base.model.primitives.WorkspaceType;
-import com.google.idea.blaze.base.sync.projectview.WorkspaceLanguageSettings;
 import com.google.idea.common.experiments.ExperimentService;
 import com.google.idea.common.experiments.MockExperimentService;
 import com.google.repackaged.devtools.intellij.ideinfo.IntellijIdeInfo;
@@ -74,11 +70,7 @@
                     .setJavaPackage("package"))
             .build();
 
-    WorkspaceLanguageSettings workspaceLanguageSettings =
-        new WorkspaceLanguageSettings(
-            WorkspaceType.ANDROID, ImmutableSet.of(LanguageClass.ANDROID));
-    TargetIdeInfo target =
-        IdeInfoFromProtobuf.makeTargetIdeInfo(workspaceLanguageSettings, ideProto);
+    TargetIdeInfo target = IdeInfoFromProtobuf.makeTargetIdeInfo(ideProto);
     TestUtils.assertIsSerializable(target);
   }
 
@@ -88,7 +80,7 @@
     state.fileToTargetMapKey =
         ImmutableMap.of(
             new File("fileName"),
-            TargetIdeInfo.builder().setLabel(new Label("//test:test")).build().key);
+            TargetIdeInfo.builder().setLabel(Label.create("//test:test")).build().key);
     state.fileState = ImmutableMap.of();
     state.targetMap =
         new TargetMap(ImmutableMap.of()); // Tested separately in testRuleIdeInfoIsSerializable
diff --git a/base/tests/unittests/com/google/idea/blaze/base/sync/aspects/UnfilteredCompilerOptionsTest.java b/base/tests/unittests/com/google/idea/blaze/base/sync/aspects/UnfilteredCompilerOptionsTest.java
index fb09872..343f562 100644
--- a/base/tests/unittests/com/google/idea/blaze/base/sync/aspects/UnfilteredCompilerOptionsTest.java
+++ b/base/tests/unittests/com/google/idea/blaze/base/sync/aspects/UnfilteredCompilerOptionsTest.java
@@ -18,7 +18,6 @@
 import static com.google.common.truth.Truth.assertThat;
 
 import com.google.common.collect.ImmutableList;
-import com.google.common.collect.Lists;
 import com.google.idea.blaze.base.BlazeTestCase;
 import java.util.List;
 import org.junit.Test;
@@ -41,13 +40,17 @@
             "sys3/inc1",
             "-isystm",
             "sys4/inc1");
-    List<String> sysIncludes = Lists.newArrayList();
-    List<String> flags = Lists.newArrayList();
-    UnfilteredCompilerOptions.splitUnfilteredCompilerOptions(unfilteredOptions, sysIncludes, flags);
+    UnfilteredCompilerOptions compilerOptions =
+        UnfilteredCompilerOptions.builder()
+            .registerSingleOrSplitOption("-isystem")
+            .build(unfilteredOptions);
 
-    assertThat(sysIncludes).containsExactly("sys/inc1", "sys2/inc1", "sys3/inc1");
+    List<String> sysIncludes = compilerOptions.getExtractedOptionValues("-isystem");
+    List<String> flags = compilerOptions.getUninterpretedOptions();
 
-    assertThat(flags).containsExactly("-VER2", "-isystm", "sys4/inc1");
+    assertThat(sysIncludes).containsExactly("sys/inc1", "sys2/inc1", "sys3/inc1").inOrder();
+
+    assertThat(flags).containsExactly("-VER2", "-isystm", "sys4/inc1").inOrder();
   }
 
   @Test
@@ -55,12 +58,47 @@
     ImmutableList<String> unfilteredOptions =
         ImmutableList.of(
             "-isystem", "sys/inc1", "-VER2", "-isystemsys2/inc1", "-isystem", "sys3/inc1");
-    List<String> sysIncludes = Lists.newArrayList();
-    List<String> flags = Lists.newArrayList();
-    UnfilteredCompilerOptions.splitUnfilteredCompilerOptions(unfilteredOptions, sysIncludes, flags);
+    UnfilteredCompilerOptions compilerOptions =
+        UnfilteredCompilerOptions.builder()
+            .registerSingleOrSplitOption("-isystem")
+            .build(unfilteredOptions);
 
-    assertThat(sysIncludes).containsExactly("sys/inc1", "sys2/inc1", "sys3/inc1");
+    List<String> sysIncludes = compilerOptions.getExtractedOptionValues("-isystem");
+    List<String> flags = compilerOptions.getUninterpretedOptions();
+    assertThat(sysIncludes).containsExactly("sys/inc1", "sys2/inc1", "sys3/inc1").inOrder();
 
-    assertThat(flags).containsExactly("-VER2");
+    assertThat(flags).containsExactly("-VER2").inOrder();
+  }
+
+  @Test
+  public void testMultipleFlagsToExtract() {
+    ImmutableList<String> unfilteredOptions =
+        ImmutableList.of(
+            "-I",
+            "foo/headers1",
+            "-fno-exceptions",
+            "-Werror",
+            "-DMACRO1=1",
+            "-D",
+            "MACRO2",
+            "-Ifoo/headers2",
+            "-I=sysroot_header",
+            "-Wall",
+            "-I",
+            "foo/headers3");
+    UnfilteredCompilerOptions compilerOptions =
+        UnfilteredCompilerOptions.builder()
+            .registerSingleOrSplitOption("-I")
+            .registerSingleOrSplitOption("-D")
+            .build(unfilteredOptions);
+
+    List<String> defines = compilerOptions.getExtractedOptionValues("-D");
+    List<String> includes = compilerOptions.getExtractedOptionValues("-I");
+    List<String> flags = compilerOptions.getUninterpretedOptions();
+    assertThat(includes)
+        .containsExactly("foo/headers1", "foo/headers2", "=sysroot_header", "foo/headers3")
+        .inOrder();
+    assertThat(defines).containsExactly("MACRO1=1", "MACRO2").inOrder();
+    assertThat(flags).containsExactly("-fno-exceptions", "-Werror", "-Wall").inOrder();
   }
 }
diff --git a/base/tests/unittests/com/google/idea/blaze/base/sync/projectview/RelatedWorkspacePathFinderTest.java b/base/tests/unittests/com/google/idea/blaze/base/sync/projectview/RelatedWorkspacePathFinderTest.java
new file mode 100644
index 0000000..15a6d84
--- /dev/null
+++ b/base/tests/unittests/com/google/idea/blaze/base/sync/projectview/RelatedWorkspacePathFinderTest.java
@@ -0,0 +1,192 @@
+/*
+ * Copyright 2017 The Bazel Authors. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.idea.blaze.base.sync.projectview;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.google.common.collect.ImmutableSet;
+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.sync.workspace.WorkspacePathResolver;
+import com.google.idea.blaze.base.sync.workspace.WorkspacePathResolverImpl;
+import java.io.File;
+import java.io.IOException;
+import java.util.HashSet;
+import java.util.Set;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** Unit tests for {@link RelatedWorkspacePathFinder} */
+@RunWith(JUnit4.class)
+public class RelatedWorkspacePathFinderTest {
+
+  private static final File WORKSPACE_ROOT = new File("/workspace");
+
+  private MockFileAttributeProvider files;
+  private RelatedWorkspacePathFinder relatedPathFinder;
+  private WorkspacePathResolver workspacePathResolver;
+
+  @Before
+  public void setUp() throws IOException {
+    files = new MockFileAttributeProvider();
+    relatedPathFinder = new RelatedWorkspacePathFinder(files);
+    workspacePathResolver = new WorkspacePathResolverImpl(new WorkspaceRoot(WORKSPACE_ROOT));
+  }
+
+  @Test
+  public void initialJavaDirectory() throws Exception {
+    files.mkdirs(new File(WORKSPACE_ROOT, "java/com/google"));
+    files.mkdirs(new File(WORKSPACE_ROOT, "javatests/com/google"));
+
+    WorkspacePath initialPath = new WorkspacePath("java/com/google");
+    ImmutableSet<WorkspacePath> relatedPaths =
+        relatedPathFinder.findRelatedWorkspaceDirectories(workspacePathResolver, initialPath);
+
+    assertThat(relatedPaths).hasSize(1);
+    assertThat(relatedPaths).containsExactly(new WorkspacePath("javatests/com/google"));
+  }
+
+  @Test
+  public void middleJavaDirectory() throws Exception {
+    files.mkdirs(new File(WORKSPACE_ROOT, "srcs/java/com/google"));
+    files.mkdirs(new File(WORKSPACE_ROOT, "srcs/javatests/com/google"));
+
+    WorkspacePath initialPath = new WorkspacePath("srcs/java/com/google");
+    ImmutableSet<WorkspacePath> relatedPaths =
+        relatedPathFinder.findRelatedWorkspaceDirectories(workspacePathResolver, initialPath);
+
+    assertThat(relatedPaths).hasSize(1);
+    assertThat(relatedPaths).containsExactly(new WorkspacePath("srcs/javatests/com/google"));
+  }
+
+  @Test
+  public void finalJavaDirectory() throws Exception {
+    files.mkdirs(new File(WORKSPACE_ROOT, "srcs/java"));
+    files.mkdirs(new File(WORKSPACE_ROOT, "srcs/javatests"));
+
+    WorkspacePath initialPath = new WorkspacePath("srcs/java");
+    ImmutableSet<WorkspacePath> relatedPaths =
+        relatedPathFinder.findRelatedWorkspaceDirectories(workspacePathResolver, initialPath);
+
+    assertThat(relatedPaths).hasSize(1);
+    assertThat(relatedPaths).containsExactly(new WorkspacePath("srcs/javatests"));
+  }
+
+  @Test
+  public void noJavaDirectory() throws Exception {
+    files.mkdirs(new File(WORKSPACE_ROOT, "srcs/com/google"));
+
+    WorkspacePath initialPath = new WorkspacePath("srcs/com/google");
+    ImmutableSet<WorkspacePath> relatedPaths =
+        relatedPathFinder.findRelatedWorkspaceDirectories(workspacePathResolver, initialPath);
+
+    assertThat(relatedPaths).isEmpty();
+  }
+
+  @Test
+  public void javatestsDirectoryDoesNotExist() throws Exception {
+    files.mkdirs(new File(WORKSPACE_ROOT, "java/com/google"));
+
+    WorkspacePath initialPath = new WorkspacePath("java/com/google");
+    ImmutableSet<WorkspacePath> relatedPaths =
+        relatedPathFinder.findRelatedWorkspaceDirectories(workspacePathResolver, initialPath);
+
+    assertThat(relatedPaths).isEmpty();
+  }
+
+  @Test
+  public void javaInMiddleOfWord() throws Exception {
+    files.mkdirs(new File(WORKSPACE_ROOT, "srcs/cooljavastuff/com/google"));
+    files.mkdirs(new File(WORKSPACE_ROOT, "srcs/cooljavatestsstuff/com/google"));
+
+    WorkspacePath initialPath = new WorkspacePath("java/com/google");
+    ImmutableSet<WorkspacePath> relatedPaths =
+        relatedPathFinder.findRelatedWorkspaceDirectories(workspacePathResolver, initialPath);
+
+    assertThat(relatedPaths).isEmpty();
+  }
+
+  @Test
+  public void javatestsExistsButSubdirectoryDoesNotExist() throws Exception {
+    files.mkdirs(new File(WORKSPACE_ROOT, "java/com/google"));
+    files.mkdirs(new File(WORKSPACE_ROOT, "javatests/com"));
+
+    WorkspacePath initialPath = new WorkspacePath("java/com/google");
+    ImmutableSet<WorkspacePath> relatedPaths =
+        relatedPathFinder.findRelatedWorkspaceDirectories(workspacePathResolver, initialPath);
+
+    assertThat(relatedPaths).isEmpty();
+  }
+
+  @Test
+  public void onlyReplacesFirstFoundJavaPath() throws Exception {
+    files.mkdirs(new File(WORKSPACE_ROOT, "java/src/java"));
+    files.mkdirs(new File(WORKSPACE_ROOT, "javatests/src/java"));
+    files.mkdirs(new File(WORKSPACE_ROOT, "javatests/src/javatests"));
+
+    WorkspacePath initialPath = new WorkspacePath("java/src/java");
+    ImmutableSet<WorkspacePath> relatedPaths =
+        relatedPathFinder.findRelatedWorkspaceDirectories(workspacePathResolver, initialPath);
+
+    assertThat(relatedPaths).hasSize(1);
+    assertThat(relatedPaths).containsExactly(new WorkspacePath("javatests/src/java"));
+  }
+
+  @Test
+  public void skipsFirstJavaIfMatchingJavatestsIsNotFound() throws Exception {
+    files.mkdirs(new File(WORKSPACE_ROOT, "java/src/java"));
+    files.mkdirs(new File(WORKSPACE_ROOT, "java/src/javatests"));
+
+    WorkspacePath initialPath = new WorkspacePath("java/src/java");
+    ImmutableSet<WorkspacePath> relatedPaths =
+        relatedPathFinder.findRelatedWorkspaceDirectories(workspacePathResolver, initialPath);
+
+    assertThat(relatedPaths).hasSize(1);
+    assertThat(relatedPaths).containsExactly(new WorkspacePath("java/src/javatests"));
+  }
+
+  @Test
+  public void javatestsIsNotRelatedToJava() throws Exception {
+    files.mkdirs(new File(WORKSPACE_ROOT, "java/com/google"));
+    files.mkdirs(new File(WORKSPACE_ROOT, "javatests/com/google"));
+
+    WorkspacePath initialPath = new WorkspacePath("javatests/com/google");
+    ImmutableSet<WorkspacePath> relatedPaths =
+        relatedPathFinder.findRelatedWorkspaceDirectories(workspacePathResolver, initialPath);
+
+    assertThat(relatedPaths).isEmpty();
+  }
+
+  private static class MockFileAttributeProvider extends FileAttributeProvider {
+
+    private final Set<File> existingFiles = new HashSet<>();
+
+    @Override
+    public boolean exists(File file) {
+      return existingFiles.contains(file);
+    }
+
+    void mkdirs(File file) {
+      while (file != null && !existingFiles.contains(file)) {
+        existingFiles.add(file);
+        file = file.getParentFile();
+      }
+    }
+  }
+}
diff --git a/base/tests/unittests/com/google/idea/blaze/base/sync/sharding/PartitionTargetsTest.java b/base/tests/unittests/com/google/idea/blaze/base/sync/sharding/PartitionTargetsTest.java
new file mode 100644
index 0000000..7da1a7a
--- /dev/null
+++ b/base/tests/unittests/com/google/idea/blaze/base/sync/sharding/PartitionTargetsTest.java
@@ -0,0 +1,95 @@
+/*
+ * Copyright 2017 The Bazel Authors. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.idea.blaze.base.sync.sharding;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.google.common.collect.ImmutableList;
+import com.google.idea.blaze.base.model.primitives.TargetExpression;
+import java.util.List;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** Test that targets are correctly partitioned in {@link BlazeBuildTargetSharder#shardTargets}. */
+@RunWith(JUnit4.class)
+public class PartitionTargetsTest {
+
+  @Test
+  public void testShardSizeRespected() {
+    List<TargetExpression> targets =
+        ImmutableList.of(
+            TargetExpression.fromString("//java/com/google:one"),
+            TargetExpression.fromString("//java/com/google:two"),
+            TargetExpression.fromString("//java/com/google:three"),
+            TargetExpression.fromString("//java/com/google:four"),
+            TargetExpression.fromString("//java/com/google:five"));
+    ShardedTargetList shards = BlazeBuildTargetSharder.shardTargets(targets, 2);
+    assertThat(shards.shardedTargets).hasSize(3);
+    assertThat(shards.shardedTargets.get(0)).hasSize(2);
+    assertThat(shards.shardedTargets.get(1)).hasSize(2);
+    assertThat(shards.shardedTargets.get(2)).hasSize(1);
+
+    shards = BlazeBuildTargetSharder.shardTargets(targets, 4);
+    assertThat(shards.shardedTargets).hasSize(2);
+    assertThat(shards.shardedTargets.get(0)).hasSize(4);
+    assertThat(shards.shardedTargets.get(1)).hasSize(1);
+
+    shards = BlazeBuildTargetSharder.shardTargets(targets, 100);
+    assertThat(shards.shardedTargets).hasSize(1);
+    assertThat(shards.shardedTargets.get(0)).hasSize(5);
+  }
+
+  @Test
+  public void testAllSubsequentExcludedTargetsAppendedToShards() {
+    List<TargetExpression> targets =
+        ImmutableList.of(
+            TargetExpression.fromString("//java/com/google:one"),
+            TargetExpression.fromString("-//java/com/google:two"),
+            TargetExpression.fromString("//java/com/google:three"),
+            TargetExpression.fromString("-//java/com/google:four"),
+            TargetExpression.fromString("//java/com/google:five"),
+            TargetExpression.fromString("-//java/com/google:six"));
+    ShardedTargetList shards = BlazeBuildTargetSharder.shardTargets(targets, 3);
+    assertThat(shards.shardedTargets).hasSize(2);
+    assertThat(shards.shardedTargets.get(0)).hasSize(5);
+    assertThat(shards.shardedTargets.get(0))
+        .isEqualTo(
+            ImmutableList.of(
+                TargetExpression.fromString("//java/com/google:one"),
+                TargetExpression.fromString("-//java/com/google:two"),
+                TargetExpression.fromString("//java/com/google:three"),
+                TargetExpression.fromString("-//java/com/google:four"),
+                TargetExpression.fromString("-//java/com/google:six")));
+    assertThat(shards.shardedTargets.get(1)).hasSize(3);
+    assertThat(shards.shardedTargets.get(1))
+        .containsExactly(
+            TargetExpression.fromString("-//java/com/google:four"),
+            TargetExpression.fromString("//java/com/google:five"),
+            TargetExpression.fromString("-//java/com/google:six"))
+        .inOrder();
+
+    shards = BlazeBuildTargetSharder.shardTargets(targets, 1);
+    assertThat(shards.shardedTargets).hasSize(6);
+    assertThat(shards.shardedTargets.get(0))
+        .containsExactly(
+            TargetExpression.fromString("//java/com/google:one"),
+            TargetExpression.fromString("-//java/com/google:two"),
+            TargetExpression.fromString("-//java/com/google:four"),
+            TargetExpression.fromString("-//java/com/google:six"))
+        .inOrder();
+  }
+}
diff --git a/base/tests/unittests/com/google/idea/blaze/base/sync/sharding/QueryResultLineProcessorTest.java b/base/tests/unittests/com/google/idea/blaze/base/sync/sharding/QueryResultLineProcessorTest.java
new file mode 100644
index 0000000..a984e94
--- /dev/null
+++ b/base/tests/unittests/com/google/idea/blaze/base/sync/sharding/QueryResultLineProcessorTest.java
@@ -0,0 +1,107 @@
+/*
+ * Copyright 2017 The Bazel Authors. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.idea.blaze.base.sync.sharding;
+
+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.TargetExpression;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** Unit tests for {@link QueryResultLineProcessor}. */
+@RunWith(JUnit4.class)
+public class QueryResultLineProcessorTest extends BlazeTestCase {
+
+  @Test
+  public void testRecognizesStandardResultLines() {
+    ImmutableList.Builder<TargetExpression> output = ImmutableList.builder();
+    QueryResultLineProcessor processor = new QueryResultLineProcessor(output, x -> true);
+
+    processor.processLine("css_library rule //java/com/google/foo/styles:global");
+    processor.processLine("java_library rule //java/com/google/bar/console:runtime_deps");
+
+    ImmutableList<TargetExpression> parsedTargets = output.build();
+    assertThat(parsedTargets)
+        .containsExactly(
+            TargetExpression.fromString("//java/com/google/foo/styles:global"),
+            TargetExpression.fromString("//java/com/google/bar/console:runtime_deps"));
+  }
+
+  @Test
+  public void testIgnoresNonRules() {
+    ImmutableList.Builder<TargetExpression> output = ImmutableList.builder();
+    QueryResultLineProcessor processor = new QueryResultLineProcessor(output, x -> true);
+
+    processor.processLine("generated file //java/com/google/foo:libthrowable_utils.jar");
+    processor.processLine("source file //java/com/google/foo:BUILD");
+    processor.processLine("package group //java/com/google/foo:packages");
+
+    assertThat(output.build()).isEmpty();
+  }
+
+  @Test
+  public void testFilterRuleTypes() {
+    ImmutableSet<String> acceptedRuleTypes =
+        ImmutableSet.of("java_library", "custom_type", "sh_test");
+    ImmutableList.Builder<TargetExpression> output = ImmutableList.builder();
+    QueryResultLineProcessor processor =
+        new QueryResultLineProcessor(output, t -> acceptedRuleTypes.contains(t.ruleType));
+
+    processor.processLine("css_library rule //java/com/google/foo/styles:global");
+    processor.processLine("java_library rule //java/com/google/bar/console:runtime_deps");
+    processor.processLine("java_test rule //java/com/google/bar/console:test1");
+    processor.processLine("test_suite rule //java/com/google/bar/console:all_tests");
+    processor.processLine("custom_type rule //java/com/google/bar/console:custom");
+    processor.processLine("sh_test rule //java/com/google/bar/console:sh_test");
+
+    assertThat(output.build())
+        .containsExactly(
+            TargetExpression.fromString("//java/com/google/bar/console:runtime_deps"),
+            TargetExpression.fromString("//java/com/google/bar/console:custom"),
+            TargetExpression.fromString("//java/com/google/bar/console:sh_test"));
+  }
+
+  @Test
+  public void testFilterRuleTypesRetainingExplicitlySpecifiedTargets() {
+    ImmutableSet<String> acceptedRuleTypes =
+        ImmutableSet.of("java_library", "custom_type", "sh_test");
+    ImmutableSet<String> explicitTargets = ImmutableSet.of("//java/com/google/foo/styles:global");
+
+    ImmutableList.Builder<TargetExpression> output = ImmutableList.builder();
+    QueryResultLineProcessor processor =
+        new QueryResultLineProcessor(
+            output,
+            t -> explicitTargets.contains(t.label) || acceptedRuleTypes.contains(t.ruleType));
+
+    processor.processLine("css_library rule //java/com/google/foo/styles:global");
+    processor.processLine("java_library rule //java/com/google/bar/console:runtime_deps");
+    processor.processLine("java_test rule //java/com/google/bar/console:test1");
+    processor.processLine("test_suite rule //java/com/google/bar/console:all_tests");
+    processor.processLine("custom_type rule //java/com/google/bar/console:custom");
+    processor.processLine("sh_test rule //java/com/google/bar/console:sh_test");
+
+    assertThat(output.build())
+        .containsExactly(
+            TargetExpression.fromString("//java/com/google/foo/styles:global"),
+            TargetExpression.fromString("//java/com/google/bar/console:runtime_deps"),
+            TargetExpression.fromString("//java/com/google/bar/console:custom"),
+            TargetExpression.fromString("//java/com/google/bar/console:sh_test"));
+  }
+}
diff --git a/base/tests/unittests/com/google/idea/blaze/base/sync/sharding/WildcardTargetPatternTest.java b/base/tests/unittests/com/google/idea/blaze/base/sync/sharding/WildcardTargetPatternTest.java
new file mode 100644
index 0000000..e3d78b8
--- /dev/null
+++ b/base/tests/unittests/com/google/idea/blaze/base/sync/sharding/WildcardTargetPatternTest.java
@@ -0,0 +1,115 @@
+/*
+ * Copyright 2017 The Bazel Authors. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.idea.blaze.base.sync.sharding;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.google.idea.blaze.base.model.primitives.TargetExpression;
+import com.google.idea.blaze.base.model.primitives.WorkspacePath;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** Unit tests for {@link WildcardTargetPattern}. */
+@RunWith(JUnit4.class)
+public class WildcardTargetPatternTest {
+
+  @Test
+  public void testRecursiveWildcardPattern() {
+    TargetExpression target = TargetExpression.fromString("//java/com/google/...");
+    WildcardTargetPattern wildcardPattern = WildcardTargetPattern.fromExpression(target);
+    assertThat(wildcardPattern).isNotNull();
+    assertThat(wildcardPattern.coversPackage(new WorkspacePath("java/com/google"))).isTrue();
+    assertThat(wildcardPattern.coversPackage(new WorkspacePath("java/com/google/foo"))).isTrue();
+    assertThat(wildcardPattern.isRecursive()).isTrue();
+    assertThat(wildcardPattern.getBasePackage()).isEqualTo(new WorkspacePath("java/com/google"));
+    assertThat(wildcardPattern.rulesOnly()).isTrue();
+  }
+
+  @Test
+  public void testRecursiveWildcardPatternAlternativeFormat() {
+    TargetExpression target = TargetExpression.fromString("//java/com/google/...:all");
+    WildcardTargetPattern wildcardPattern = WildcardTargetPattern.fromExpression(target);
+    assertThat(wildcardPattern).isNotNull();
+    assertThat(wildcardPattern.coversPackage(new WorkspacePath("java/com/google"))).isTrue();
+    assertThat(wildcardPattern.coversPackage(new WorkspacePath("java/com/google/foo"))).isTrue();
+    assertThat(wildcardPattern.isRecursive()).isTrue();
+    assertThat(wildcardPattern.getBasePackage()).isEqualTo(new WorkspacePath("java/com/google"));
+    assertThat(wildcardPattern.rulesOnly()).isTrue();
+  }
+
+  @Test
+  public void testRecursiveWildcardPatternAllTargets() {
+    TargetExpression target = TargetExpression.fromString("//java/com/google/...:all-targets");
+    WildcardTargetPattern wildcardPattern = WildcardTargetPattern.fromExpression(target);
+    assertThat(wildcardPattern).isNotNull();
+    assertThat(wildcardPattern.coversPackage(new WorkspacePath("java/com/google"))).isTrue();
+    assertThat(wildcardPattern.coversPackage(new WorkspacePath("java/com/google/foo"))).isTrue();
+    assertThat(wildcardPattern.isRecursive()).isTrue();
+    assertThat(wildcardPattern.getBasePackage()).isEqualTo(new WorkspacePath("java/com/google"));
+    assertThat(wildcardPattern.rulesOnly()).isFalse();
+  }
+
+  @Test
+  public void testRecursiveWildcardPatternAllTargetsAlternativeFormat() {
+    TargetExpression target = TargetExpression.fromString("//java/com/google/...:*");
+    WildcardTargetPattern wildcardPattern = WildcardTargetPattern.fromExpression(target);
+    assertThat(wildcardPattern).isNotNull();
+    assertThat(wildcardPattern.coversPackage(new WorkspacePath("java/com/google"))).isTrue();
+    assertThat(wildcardPattern.coversPackage(new WorkspacePath("java/com/google/foo"))).isTrue();
+    assertThat(wildcardPattern.isRecursive()).isTrue();
+    assertThat(wildcardPattern.getBasePackage()).isEqualTo(new WorkspacePath("java/com/google"));
+    assertThat(wildcardPattern.rulesOnly()).isFalse();
+  }
+
+  @Test
+  public void testNonRecursiveAllTargetsWildcardPattern() {
+    TargetExpression target = TargetExpression.fromString("//java/com/google:*");
+    WildcardTargetPattern wildcardPattern = WildcardTargetPattern.fromExpression(target);
+    assertThat(wildcardPattern).isNotNull();
+    assertThat(wildcardPattern.coversPackage(new WorkspacePath("java/com/google"))).isTrue();
+    assertThat(wildcardPattern.coversPackage(new WorkspacePath("java/com/google/foo"))).isFalse();
+    assertThat(wildcardPattern.isRecursive()).isFalse();
+    assertThat(wildcardPattern.getBasePackage()).isEqualTo(new WorkspacePath("java/com/google"));
+    assertThat(wildcardPattern.rulesOnly()).isFalse();
+  }
+
+  @Test
+  public void testNonRecursiveWildcardPattern() {
+    TargetExpression target = TargetExpression.fromString("//java/com/google:all");
+    WildcardTargetPattern wildcardPattern = WildcardTargetPattern.fromExpression(target);
+    assertThat(wildcardPattern).isNotNull();
+    assertThat(wildcardPattern.coversPackage(new WorkspacePath("java/com/google"))).isTrue();
+    assertThat(wildcardPattern.coversPackage(new WorkspacePath("java/com/google/foo"))).isFalse();
+    assertThat(wildcardPattern.isRecursive()).isFalse();
+    assertThat(wildcardPattern.getBasePackage()).isEqualTo(new WorkspacePath("java/com/google"));
+    assertThat(wildcardPattern.rulesOnly()).isTrue();
+  }
+
+  @Test
+  public void testNonWildcardPattern() {
+    TargetExpression target = TargetExpression.fromString("//java/com/google:single_target");
+    WildcardTargetPattern wildcardPattern = WildcardTargetPattern.fromExpression(target);
+    assertThat(wildcardPattern).isNull();
+  }
+
+  @Test
+  public void testNonWildcardImplicitTargetName() {
+    TargetExpression target = TargetExpression.fromString("//java/com/google/foo");
+    WildcardTargetPattern wildcardPattern = WildcardTargetPattern.fromExpression(target);
+    assertThat(wildcardPattern).isNull();
+  }
+}
diff --git a/base/tests/unittests/com/google/idea/blaze/base/sync/workspace/ArtifactLocationDecoderTest.java b/base/tests/unittests/com/google/idea/blaze/base/sync/workspace/ArtifactLocationDecoderTest.java
index a7c7816..495f9dc 100644
--- a/base/tests/unittests/com/google/idea/blaze/base/sync/workspace/ArtifactLocationDecoderTest.java
+++ b/base/tests/unittests/com/google/idea/blaze/base/sync/workspace/ArtifactLocationDecoderTest.java
@@ -17,18 +17,11 @@
 
 import static com.google.common.truth.Truth.assertThat;
 
-import com.google.common.collect.ImmutableList;
-import com.google.common.collect.Lists;
-import com.google.common.collect.Sets;
 import com.google.idea.blaze.base.BlazeTestCase;
+import com.google.idea.blaze.base.command.info.BlazeInfo;
 import com.google.idea.blaze.base.ideinfo.ArtifactLocation;
-import com.google.idea.blaze.base.io.FileAttributeProvider;
-import com.google.idea.blaze.base.model.primitives.ExecutionRootPath;
-import com.google.idea.blaze.base.model.primitives.WorkspaceRoot;
-import java.io.File;
-import java.util.List;
-import java.util.Set;
-import org.jetbrains.annotations.NotNull;
+import com.google.idea.blaze.base.sync.aspects.IdeInfoFromProtobuf;
+import com.google.repackaged.devtools.intellij.ideinfo.IntellijIdeInfo;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.junit.runners.JUnit4;
@@ -37,79 +30,8 @@
 @RunWith(JUnit4.class)
 public class ArtifactLocationDecoderTest extends BlazeTestCase {
 
-  private static final String EXECUTION_ROOT = "/path/to/_blaze_user/1234bf129e/root";
   private static final String OUTPUT_BASE = "/path/to/_blaze_user/1234bf129e";
-
-  static class MockFileAttributeProvider extends FileAttributeProvider {
-    final Set<File> files = Sets.newHashSet();
-
-    void addFiles(@NotNull File... files) {
-      this.files.addAll(Lists.newArrayList(files));
-    }
-
-    @Override
-    public boolean exists(@NotNull File file) {
-      return files.contains(file);
-    }
-  }
-
-  private MockFileAttributeProvider fileChecker;
-
-  @Override
-  protected void initTest(
-      @NotNull Container applicationServices, @NotNull Container projectServices) {
-    super.initTest(applicationServices, projectServices);
-
-    fileChecker = new MockFileAttributeProvider();
-    applicationServices.register(FileAttributeProvider.class, fileChecker);
-  }
-
-  @Test
-  public void testManualPackagePaths() throws Exception {
-    List<File> packagePaths =
-        ImmutableList.of(
-            new File("/path/to"),
-            new File("/path/to/READONLY/root"),
-            new File("/path/to/CUSTOM/root"));
-
-    BlazeRoots blazeRoots =
-        new BlazeRoots(
-            new File(EXECUTION_ROOT),
-            packagePaths,
-            new ExecutionRootPath("root/blaze-out/crosstool/bin"),
-            new ExecutionRootPath("root/blaze-out/crosstool/genfiles"),
-            new File(OUTPUT_BASE));
-
-    fileChecker.addFiles(
-        new File("/path/to/com/google/Bla.java"),
-        new File("/path/to/READONLY/root/com/google/Foo.java"),
-        new File("/path/to/CUSTOM/root/com/other/Test.java"));
-
-    ArtifactLocationDecoder decoder =
-        new ArtifactLocationDecoderImpl(
-            blazeRoots,
-            new WorkspacePathResolverImpl(
-                new WorkspaceRoot(new File("/path/to/root")), blazeRoots));
-
-    ArtifactLocation blah =
-        ArtifactLocation.builder().setRelativePath("com/google/Bla.java").setIsSource(true).build();
-    assertThat(decoder.decode(blah).getPath()).isEqualTo("/path/to/com/google/Bla.java");
-
-    ArtifactLocation foo =
-        ArtifactLocation.builder().setRelativePath("com/google/Foo.java").setIsSource(true).build();
-    assertThat(decoder.decode(foo).getPath())
-        .isEqualTo("/path/to/READONLY/root/com/google/Foo.java");
-
-    ArtifactLocation test =
-        ArtifactLocation.builder().setRelativePath("com/other/Test.java").setIsSource(true).build();
-    assertThat(decoder.decode(test).getPath())
-        .isEqualTo("/path/to/CUSTOM/root/com/other/Test.java");
-
-    ArtifactLocation.Builder temp =
-        ArtifactLocation.builder().setRelativePath("third_party/other/Temp.java").setIsSource(true);
-    assertThat(decoder.decode(temp.build()).getPath())
-        .isEqualTo("/path/to/third_party/other/Temp.java");
-  }
+  private static final String EXECUTION_ROOT = OUTPUT_BASE + "/execroot/my_proj";
 
   @Test
   public void testGeneratedArtifact() throws Exception {
@@ -122,12 +44,11 @@
 
     ArtifactLocationDecoder decoder =
         new ArtifactLocationDecoderImpl(
-            new BlazeRoots(
-                new File(EXECUTION_ROOT),
-                ImmutableList.of(new File("/path/to/root")),
-                new ExecutionRootPath("root/blaze-out/crosstool/bin"),
-                new ExecutionRootPath("root/blaze-out/crosstool/genfiles"),
-                new File(OUTPUT_BASE)),
+            BlazeInfo.createMockBlazeInfo(
+                OUTPUT_BASE,
+                EXECUTION_ROOT,
+                EXECUTION_ROOT + "/blaze-out/crosstool/bin",
+                EXECUTION_ROOT + "/blaze-out/crosstool/genfiles"),
             null);
 
     assertThat(decoder.decode(artifactLocation).getPath())
@@ -135,25 +56,115 @@
   }
 
   @Test
-  public void testExternalArtifact() throws Exception {
+  public void testExternalSourceArtifactOldFormat() throws Exception {
     ArtifactLocation artifactLocation =
-        ArtifactLocation.builder()
-            .setRelativePath("external/com/google/Bla.java")
-            .setIsSource(true)
-            .setIsExternal(true)
-            .build();
+        IdeInfoFromProtobuf.makeArtifactLocation(
+            IntellijIdeInfo.ArtifactLocation.newBuilder()
+                .setRelativePath("external/repo_name/com/google/Bla.java")
+                .setIsSource(true)
+                .setIsExternal(true)
+                .build());
+
+    assertThat(artifactLocation.getRelativePath()).isEqualTo("com/google/Bla.java");
+    assertThat(artifactLocation.getExecutionRootRelativePath())
+        .isEqualTo("external/repo_name/com/google/Bla.java");
 
     ArtifactLocationDecoder decoder =
         new ArtifactLocationDecoderImpl(
-            new BlazeRoots(
-                new File(EXECUTION_ROOT),
-                ImmutableList.of(new File("/path/to/root")),
-                new ExecutionRootPath("root/blaze-out/crosstool/bin"),
-                new ExecutionRootPath("root/blaze-out/crosstool/genfiles"),
-                new File(OUTPUT_BASE)),
+            BlazeInfo.createMockBlazeInfo(
+                OUTPUT_BASE,
+                EXECUTION_ROOT,
+                EXECUTION_ROOT + "/blaze-out/crosstool/bin",
+                EXECUTION_ROOT + "/blaze-out/crosstool/genfiles"),
             null);
 
     assertThat(decoder.decode(artifactLocation).getPath())
-        .isEqualTo(OUTPUT_BASE + "/external/com/google/Bla.java");
+        .isEqualTo(EXECUTION_ROOT + "/external/repo_name/com/google/Bla.java");
+  }
+
+  @Test
+  public void testExternalDerivedArtifactOldFormat() throws Exception {
+    ArtifactLocation artifactLocation =
+        IdeInfoFromProtobuf.makeArtifactLocation(
+            IntellijIdeInfo.ArtifactLocation.newBuilder()
+                .setRelativePath("external/repo_name/com/google/Bla.java")
+                .setRootExecutionPathFragment("blaze-out/crosstool/bin")
+                .setIsSource(false)
+                .setIsExternal(true)
+                .build());
+
+    assertThat(artifactLocation.getRelativePath()).isEqualTo("com/google/Bla.java");
+    assertThat(artifactLocation.getExecutionRootRelativePath())
+        .isEqualTo("blaze-out/crosstool/bin/external/repo_name/com/google/Bla.java");
+
+    ArtifactLocationDecoder decoder =
+        new ArtifactLocationDecoderImpl(
+            BlazeInfo.createMockBlazeInfo(
+                OUTPUT_BASE,
+                EXECUTION_ROOT,
+                EXECUTION_ROOT + "/blaze-out/crosstool/bin",
+                EXECUTION_ROOT + "/blaze-out/crosstool/genfiles"),
+            null);
+
+    assertThat(decoder.decode(artifactLocation).getPath())
+        .isEqualTo(
+            EXECUTION_ROOT + "/blaze-out/crosstool/bin/external/repo_name/com/google/Bla.java");
+  }
+
+  @Test
+  public void testExternalSourceArtifactNewFormat() throws Exception {
+    ArtifactLocation artifactLocation =
+        IdeInfoFromProtobuf.makeArtifactLocation(
+            IntellijIdeInfo.ArtifactLocation.newBuilder()
+                .setRelativePath("com/google/Bla.java")
+                .setRootExecutionPathFragment("../repo_name")
+                .setIsSource(true)
+                .setIsExternal(true)
+                .setIsNewExternalVersion(true)
+                .build());
+
+    assertThat(artifactLocation.getRelativePath()).isEqualTo("com/google/Bla.java");
+    assertThat(artifactLocation.getExecutionRootRelativePath())
+        .isEqualTo("../repo_name/com/google/Bla.java");
+
+    ArtifactLocationDecoder decoder =
+        new ArtifactLocationDecoderImpl(
+            BlazeInfo.createMockBlazeInfo(
+                OUTPUT_BASE,
+                EXECUTION_ROOT,
+                EXECUTION_ROOT + "/blaze-out/crosstool/bin",
+                EXECUTION_ROOT + "/blaze-out/crosstool/genfiles"),
+            null);
+
+    assertThat(decoder.decode(artifactLocation).getPath())
+        .isEqualTo(OUTPUT_BASE + "/execroot/repo_name/com/google/Bla.java");
+  }
+
+  @Test
+  public void testExternalDerivedArtifactNewFormat() throws Exception {
+    ArtifactLocation artifactLocation =
+        IdeInfoFromProtobuf.makeArtifactLocation(
+            IntellijIdeInfo.ArtifactLocation.newBuilder()
+                .setRelativePath("com/google/Bla.java")
+                .setRootExecutionPathFragment("../repo_name/blaze-out/crosstool/bin")
+                .setIsSource(false)
+                .setIsNewExternalVersion(true)
+                .build());
+
+    assertThat(artifactLocation.getRelativePath()).isEqualTo("com/google/Bla.java");
+    assertThat(artifactLocation.getExecutionRootRelativePath())
+        .isEqualTo("../repo_name/blaze-out/crosstool/bin/com/google/Bla.java");
+
+    ArtifactLocationDecoder decoder =
+        new ArtifactLocationDecoderImpl(
+            BlazeInfo.createMockBlazeInfo(
+                OUTPUT_BASE,
+                EXECUTION_ROOT,
+                EXECUTION_ROOT + "/blaze-out/crosstool/bin",
+                EXECUTION_ROOT + "/blaze-out/crosstool/genfiles"),
+            null);
+
+    assertThat(decoder.decode(artifactLocation).getPath())
+        .isEqualTo(OUTPUT_BASE + "/execroot/repo_name/blaze-out/crosstool/bin/com/google/Bla.java");
   }
 }
diff --git a/base/tests/unittests/com/google/idea/blaze/base/sync/workspace/ExecutionRootPathResolverTest.java b/base/tests/unittests/com/google/idea/blaze/base/sync/workspace/ExecutionRootPathResolverTest.java
index f880353..afba81b 100644
--- a/base/tests/unittests/com/google/idea/blaze/base/sync/workspace/ExecutionRootPathResolverTest.java
+++ b/base/tests/unittests/com/google/idea/blaze/base/sync/workspace/ExecutionRootPathResolverTest.java
@@ -19,9 +19,13 @@
 
 import com.google.common.collect.ImmutableList;
 import com.google.idea.blaze.base.BlazeTestCase;
+import com.google.idea.blaze.base.bazel.BazelBuildSystemProvider;
+import com.google.idea.blaze.base.bazel.BuildSystemProvider;
 import com.google.idea.blaze.base.model.primitives.ExecutionRootPath;
 import com.google.idea.blaze.base.model.primitives.WorkspacePath;
 import com.google.idea.blaze.base.model.primitives.WorkspaceRoot;
+import com.google.idea.blaze.base.settings.Blaze.BuildSystem;
+import com.intellij.openapi.extensions.ExtensionPoint;
 import java.io.File;
 import org.junit.Test;
 import org.junit.runner.RunWith;
@@ -32,19 +36,23 @@
 public class ExecutionRootPathResolverTest extends BlazeTestCase {
 
   private static final WorkspaceRoot WORKSPACE_ROOT = new WorkspaceRoot(new File("/path/to/root"));
-  private static final String EXECUTION_ROOT = "/path/to/_blaze_user/1234bf129e/root";
+  private static final String EXECUTION_ROOT = "/path/to/_bazel_user/1234bf129e/root";
 
-  private static final BlazeRoots BLAZE_ROOTS =
-      new BlazeRoots(
-          new File(EXECUTION_ROOT),
-          ImmutableList.of(WORKSPACE_ROOT.directory()),
-          new ExecutionRootPath("blaze-out/crosstool/bin"),
-          new ExecutionRootPath("blaze-out/crosstool/genfiles"),
-          null);
+  private ExecutionRootPathResolver pathResolver;
 
-  private final ExecutionRootPathResolver pathResolver =
-      new ExecutionRootPathResolver(
-          BLAZE_ROOTS, new WorkspacePathResolverImpl(WORKSPACE_ROOT, BLAZE_ROOTS));
+  @Override
+  protected void initTest(Container applicationServices, Container projectServices) {
+    ExtensionPoint<BuildSystemProvider> extensionPoint =
+        registerExtensionPoint(BuildSystemProvider.EP_NAME, BuildSystemProvider.class);
+    extensionPoint.registerExtension(new BazelBuildSystemProvider());
+
+    pathResolver =
+        new ExecutionRootPathResolver(
+            BuildSystem.Bazel,
+            WORKSPACE_ROOT,
+            new File(EXECUTION_ROOT),
+            new WorkspacePathResolverImpl(WORKSPACE_ROOT));
+  }
 
   @Test
   public void testExternalWorkspacePathRelativeToExecRoot() {
@@ -57,9 +65,9 @@
   public void testGenfilesPathRelativeToExecRoot() {
     ImmutableList<File> files =
         pathResolver.resolveToIncludeDirectories(
-            new ExecutionRootPath("blaze-out/crosstool/genfiles/res/normal"));
+            new ExecutionRootPath("bazel-out/crosstool/genfiles/res/normal"));
     assertThat(files)
-        .containsExactly(new File(EXECUTION_ROOT, "blaze-out/crosstool/genfiles/res/normal"));
+        .containsExactly(new File(EXECUTION_ROOT, "bazel-out/crosstool/genfiles/res/normal"));
   }
 
   @Test
@@ -68,4 +76,14 @@
         pathResolver.resolveToIncludeDirectories(new ExecutionRootPath("tools/fast"));
     assertThat(files).containsExactly(WORKSPACE_ROOT.fileForPath(new WorkspacePath("tools/fast")));
   }
+
+  @Test
+  public void testGenfilesPathWithDifferentConfigSettingStillResolves() {
+    ImmutableList<File> files =
+        pathResolver.resolveToIncludeDirectories(
+            new ExecutionRootPath("bazel-out/arm-linux-fastbuild/genfiles/res/normal"));
+    assertThat(files)
+        .containsExactly(
+            new File(EXECUTION_ROOT, "bazel-out/arm-linux-fastbuild/genfiles/res/normal"));
+  }
 }
diff --git a/base/tests/unittests/com/google/idea/blaze/base/sync/workspace/WorkspacePathResolverImplTest.java b/base/tests/unittests/com/google/idea/blaze/base/sync/workspace/WorkspacePathResolverImplTest.java
index 282ea53..446d6ba 100644
--- a/base/tests/unittests/com/google/idea/blaze/base/sync/workspace/WorkspacePathResolverImplTest.java
+++ b/base/tests/unittests/com/google/idea/blaze/base/sync/workspace/WorkspacePathResolverImplTest.java
@@ -19,7 +19,6 @@
 
 import com.google.common.collect.ImmutableList;
 import com.google.idea.blaze.base.BlazeTestCase;
-import com.google.idea.blaze.base.model.primitives.ExecutionRootPath;
 import com.google.idea.blaze.base.model.primitives.WorkspacePath;
 import com.google.idea.blaze.base.model.primitives.WorkspaceRoot;
 import java.io.File;
@@ -31,20 +30,10 @@
 @RunWith(JUnit4.class)
 public class WorkspacePathResolverImplTest extends BlazeTestCase {
   private static final WorkspaceRoot WORKSPACE_ROOT = new WorkspaceRoot(new File("/path/to/root"));
-  private static final String EXECUTION_ROOT = "/path/to/_blaze_user/1234bf129e/root";
-
-  private static final BlazeRoots BLAZE_CITC_ROOTS =
-      new BlazeRoots(
-          new File(EXECUTION_ROOT),
-          ImmutableList.of(WORKSPACE_ROOT.directory()),
-          new ExecutionRootPath("blaze-out/crosstool/bin"),
-          new ExecutionRootPath("blaze-out/crosstool/genfiles"),
-          null);
 
   @Test
   public void testResolveToIncludeDirectories() {
-    WorkspacePathResolver workspacePathResolver =
-        new WorkspacePathResolverImpl(WORKSPACE_ROOT, BLAZE_CITC_ROOTS);
+    WorkspacePathResolver workspacePathResolver = new WorkspacePathResolverImpl(WORKSPACE_ROOT);
     ImmutableList<File> files =
         workspacePathResolver.resolveToIncludeDirectories(new WorkspacePath("tools/fast"));
     assertThat(files).containsExactly(new File("/path/to/root/tools/fast"));
@@ -52,11 +41,18 @@
 
   @Test
   public void testResolveToIncludeDirectoriesForExecRootPath() {
-    WorkspacePathResolver workspacePathResolver =
-        new WorkspacePathResolverImpl(WORKSPACE_ROOT, BLAZE_CITC_ROOTS);
+    WorkspacePathResolver workspacePathResolver = new WorkspacePathResolverImpl(WORKSPACE_ROOT);
     ImmutableList<File> files =
         workspacePathResolver.resolveToIncludeDirectories(
             new WorkspacePath("blaze-out/crosstool/bin/tools/fast"));
     assertThat(files).containsExactly(new File("/path/to/root/blaze-out/crosstool/bin/tools/fast"));
   }
+
+  @Test
+  public void testResolveToFile() {
+    WorkspacePathResolver workspacePathResolver = new WorkspacePathResolverImpl(WORKSPACE_ROOT);
+    WorkspacePath relativePath = new WorkspacePath("third_party/tools");
+    assertThat(workspacePathResolver.resolveToFile(relativePath))
+        .isEqualTo(WORKSPACE_ROOT.fileForPath(relativePath));
+  }
 }
diff --git a/base/tests/unittests/com/google/idea/blaze/base/targetmaps/ReverseDependencyMapTest.java b/base/tests/unittests/com/google/idea/blaze/base/targetmaps/ReverseDependencyMapTest.java
index 574194c..9f0b306 100644
--- a/base/tests/unittests/com/google/idea/blaze/base/targetmaps/ReverseDependencyMapTest.java
+++ b/base/tests/unittests/com/google/idea/blaze/base/targetmaps/ReverseDependencyMapTest.java
@@ -61,8 +61,8 @@
         ReverseDependencyMap.createRdepsMap(targetMap);
     assertThat(reverseDependencies)
         .containsEntry(
-            TargetKey.forPlainTarget(new Label("//l:l2")),
-            TargetKey.forPlainTarget(new Label("//l:l1")));
+            TargetKey.forPlainTarget(Label.create("//l:l2")),
+            TargetKey.forPlainTarget(Label.create("//l:l1")));
   }
 
   @Test
@@ -93,12 +93,12 @@
         ReverseDependencyMap.createRdepsMap(targetMap);
     assertThat(reverseDependencies)
         .containsEntry(
-            TargetKey.forPlainTarget(new Label("//l:l2")),
-            TargetKey.forPlainTarget(new Label("//l:l1")));
+            TargetKey.forPlainTarget(Label.create("//l:l2")),
+            TargetKey.forPlainTarget(Label.create("//l:l1")));
     assertThat(reverseDependencies)
         .containsEntry(
-            TargetKey.forPlainTarget(new Label("//l:l3")),
-            TargetKey.forPlainTarget(new Label("//l:l1")));
+            TargetKey.forPlainTarget(Label.create("//l:l3")),
+            TargetKey.forPlainTarget(Label.create("//l:l1")));
   }
 
   @Test
@@ -129,12 +129,12 @@
         ReverseDependencyMap.createRdepsMap(targetMap);
     assertThat(reverseDependencies)
         .containsEntry(
-            TargetKey.forPlainTarget(new Label("//l:l3")),
-            TargetKey.forPlainTarget(new Label("//l:l1")));
+            TargetKey.forPlainTarget(Label.create("//l:l3")),
+            TargetKey.forPlainTarget(Label.create("//l:l1")));
     assertThat(reverseDependencies)
         .containsEntry(
-            TargetKey.forPlainTarget(new Label("//l:l3")),
-            TargetKey.forPlainTarget(new Label("//l:l2")));
+            TargetKey.forPlainTarget(Label.create("//l:l3")),
+            TargetKey.forPlainTarget(Label.create("//l:l2")));
   }
 
   @Test
@@ -177,20 +177,20 @@
         ReverseDependencyMap.createRdepsMap(targetMap);
     assertThat(reverseDependencies)
         .containsEntry(
-            TargetKey.forPlainTarget(new Label("//l:l3")),
-            TargetKey.forPlainTarget(new Label("//l:l1")));
+            TargetKey.forPlainTarget(Label.create("//l:l3")),
+            TargetKey.forPlainTarget(Label.create("//l:l1")));
     assertThat(reverseDependencies)
         .containsEntry(
-            TargetKey.forPlainTarget(new Label("//l:l3")),
-            TargetKey.forPlainTarget(new Label("//l:l2")));
+            TargetKey.forPlainTarget(Label.create("//l:l3")),
+            TargetKey.forPlainTarget(Label.create("//l:l2")));
     assertThat(reverseDependencies)
         .containsEntry(
-            TargetKey.forPlainTarget(new Label("//l:l3")),
-            TargetKey.forPlainTarget(new Label("//l:l4")));
+            TargetKey.forPlainTarget(Label.create("//l:l3")),
+            TargetKey.forPlainTarget(Label.create("//l:l4")));
     assertThat(reverseDependencies)
         .containsEntry(
-            TargetKey.forPlainTarget(new Label("//l:l4")),
-            TargetKey.forPlainTarget(new Label("//l:l5")));
+            TargetKey.forPlainTarget(Label.create("//l:l4")),
+            TargetKey.forPlainTarget(Label.create("//l:l5")));
   }
 
   private static ArtifactLocation sourceRoot(String relativePath) {
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
index 922cbd1..6aa4f43 100644
--- a/base/tests/unittests/com/google/idea/blaze/base/targetmaps/TransitiveDependencyMapTest.java
+++ b/base/tests/unittests/com/google/idea/blaze/base/targetmaps/TransitiveDependencyMapTest.java
@@ -53,8 +53,8 @@
 
   @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"));
+    TargetKey simpleA = TargetKey.forPlainTarget(Label.create("//com/google/example/simple:a"));
+    TargetKey simpleB = TargetKey.forPlainTarget(Label.create("//com/google/example/simple:b"));
 
     assertThat(transitiveDependencyMap.getTransitiveDependencies(simpleA)).containsExactly(simpleB);
     assertThat(transitiveDependencyMap.getTransitiveDependencies(simpleB)).isEmpty();
@@ -62,10 +62,10 @@
 
   @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"));
+    TargetKey chainA = TargetKey.forPlainTarget(Label.create("//com/google/example/chain:a"));
+    TargetKey chainB = TargetKey.forPlainTarget(Label.create("//com/google/example/chain:b"));
+    TargetKey chainC = TargetKey.forPlainTarget(Label.create("//com/google/example/chain:c"));
+    TargetKey chainD = TargetKey.forPlainTarget(Label.create("//com/google/example/chain:d"));
 
     assertThat(transitiveDependencyMap.getTransitiveDependencies(chainA))
         .containsExactly(chainB, chainC, chainD);
@@ -77,13 +77,15 @@
 
   @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"));
+    TargetKey diamondA = TargetKey.forPlainTarget(Label.create("//com/google/example/diamond:a"));
+    TargetKey diamondB = TargetKey.forPlainTarget(Label.create("//com/google/example/diamond:b"));
+    TargetKey diamondBB = TargetKey.forPlainTarget(Label.create("//com/google/example/diamond:bb"));
+    TargetKey diamondBBB =
+        TargetKey.forPlainTarget(Label.create("//com/google/example/diamond:bbb"));
+    TargetKey diamondC = TargetKey.forPlainTarget(Label.create("//com/google/example/diamond:c"));
+    TargetKey diamondCC = TargetKey.forPlainTarget(Label.create("//com/google/example/diamond:cc"));
+    TargetKey diamondCCC =
+        TargetKey.forPlainTarget(Label.create("//com/google/example/diamond:ccc"));
 
     assertThat(transitiveDependencyMap.getTransitiveDependencies(diamondA))
         .containsExactly(diamondB, diamondBB, diamondBBB, diamondC, diamondCC, diamondCCC);
@@ -100,24 +102,24 @@
 
   @Test
   public void testGetDependencyForNonExistentTarget() {
-    TargetKey bogus = TargetKey.forPlainTarget(new Label("//com/google/fake:target"));
+    TargetKey bogus = TargetKey.forPlainTarget(Label.create("//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");
+    Label simpleA = Label.create("//com/google/example/simple:a");
+    Label simpleB = Label.create("//com/google/example/simple:b");
+    Label chainA = Label.create("//com/google/example/chain:a");
+    Label chainB = Label.create("//com/google/example/chain:b");
+    Label chainC = Label.create("//com/google/example/chain:c");
+    Label chainD = Label.create("//com/google/example/chain:d");
+    Label diamondA = Label.create("//com/google/example/diamond:a");
+    Label diamondB = Label.create("//com/google/example/diamond:b");
+    Label diamondBB = Label.create("//com/google/example/diamond:bb");
+    Label diamondBBB = Label.create("//com/google/example/diamond:bbb");
+    Label diamondC = Label.create("//com/google/example/diamond:c");
+    Label diamondCC = Label.create("//com/google/example/diamond:cc");
+    Label diamondCCC = Label.create("//com/google/example/diamond:ccc");
     return TargetMapBuilder.builder()
         .addTarget(TargetIdeInfo.builder().setLabel(simpleA).addDependency(simpleB))
         .addTarget(TargetIdeInfo.builder().setLabel(simpleB))
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 8027d58..9080df7 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
@@ -23,6 +23,7 @@
 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.sync.SyncCache;
 import com.google.idea.testing.EdtRule;
 import com.google.idea.testing.IntellijTestSetupRule;
 import com.google.idea.testing.ServiceHelper;
@@ -115,7 +116,6 @@
                 workspaceRoot.toString(),
                 "test-project",
                 projectDataDirectory.getPath(),
-                "location-hash",
                 workspaceRoot.fileForPath(new WorkspacePath("project-view-file")).getPath(),
                 buildSystem()));
 
@@ -136,6 +136,7 @@
 
   @After
   public final void tearDown() throws Exception {
+    SyncCache.getInstance(getProject()).clear();
     testFixture.tearDown();
     testFixture = null;
   }
diff --git a/base/tests/utils/integration/com/google/idea/blaze/base/TestFileSystem.java b/base/tests/utils/integration/com/google/idea/blaze/base/TestFileSystem.java
index cf15e9f..f01b053 100644
--- a/base/tests/utils/integration/com/google/idea/blaze/base/TestFileSystem.java
+++ b/base/tests/utils/integration/com/google/idea/blaze/base/TestFileSystem.java
@@ -46,6 +46,11 @@
     this.tempDirTestFixture = tempDirTestFixture;
   }
 
+  /** Returns the root directory of the file system */
+  public String getRootDir() {
+    return LightPlatformTestCase.getSourceRoot().getPath();
+  }
+
   /** Creates an empty file in the temp file system */
   public VirtualFile createFile(String filePath) {
     filePath = makePathRelativeToTestFixture(filePath);
@@ -140,7 +145,7 @@
     if (!FileUtil.isAbsolute(filePath)) {
       return filePath;
     }
-    String tempDirPath = LightPlatformTestCase.getSourceRoot().getPath();
+    String tempDirPath = getRootDir();
     assertThat(FileUtil.isAncestor(tempDirPath, filePath, true)).isTrue();
     return FileUtil.getRelativePath(tempDirPath, filePath, File.separatorChar);
   }
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 44ce8c1..7810b88 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
@@ -36,7 +36,10 @@
   @Before
   public final void doSetup() {
     BlazeProjectDataManager mockProjectDataManager =
-        new MockBlazeProjectDataManager(MockBlazeProjectDataBuilder.builder(workspaceRoot).build());
+        new MockBlazeProjectDataManager(
+            MockBlazeProjectDataBuilder.builder(workspaceRoot)
+                .setOutputBase(fileSystem.getRootDir() + "/output_base")
+                .build());
     registerProjectService(BlazeProjectDataManager.class, mockProjectDataManager);
     editorTest = new EditorTestHelper(getProject(), testFixture);
   }
diff --git a/base/tests/utils/integration/com/google/idea/blaze/base/run/producer/BlazeRunConfigurationProducerTestCase.java b/base/tests/utils/integration/com/google/idea/blaze/base/run/producer/BlazeRunConfigurationProducerTestCase.java
new file mode 100644
index 0000000..434bd3a
--- /dev/null
+++ b/base/tests/utils/integration/com/google/idea/blaze/base/run/producer/BlazeRunConfigurationProducerTestCase.java
@@ -0,0 +1,131 @@
+/*
+ * Copyright 2017 The Bazel Authors. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.idea.blaze.base.run.producer;
+
+import com.google.idea.blaze.base.BlazeIntegrationTestCase;
+import com.google.idea.blaze.base.EditorTestHelper;
+import com.google.idea.blaze.base.command.BlazeCommandName;
+import com.google.idea.blaze.base.ideinfo.ArtifactLocation;
+import com.google.idea.blaze.base.model.MockBlazeProjectDataBuilder;
+import com.google.idea.blaze.base.model.MockBlazeProjectDataManager;
+import com.google.idea.blaze.base.model.primitives.WorkspacePath;
+import com.google.idea.blaze.base.run.BlazeCommandRunConfiguration;
+import com.google.idea.blaze.base.run.state.BlazeCommandRunConfigurationCommonState;
+import com.google.idea.blaze.base.sync.data.BlazeProjectDataManager;
+import com.intellij.execution.Location;
+import com.intellij.execution.PsiLocation;
+import com.intellij.execution.RunnerAndConfigurationSettings;
+import com.intellij.execution.actions.ConfigurationContext;
+import com.intellij.execution.configurations.RunConfiguration;
+import com.intellij.ide.DataManager;
+import com.intellij.idea.CommandLineApplication.MyDataManagerImpl;
+import com.intellij.openapi.actionSystem.CommonDataKeys;
+import com.intellij.openapi.actionSystem.DataContext;
+import com.intellij.openapi.actionSystem.LangDataKeys;
+import com.intellij.openapi.module.ModuleUtil;
+import com.intellij.openapi.util.Key;
+import com.intellij.psi.PsiElement;
+import com.intellij.psi.PsiFile;
+import com.intellij.testFramework.MapDataContext;
+import javax.annotation.Nullable;
+import org.junit.Before;
+
+/** Run configuration producer integration test base */
+public class BlazeRunConfigurationProducerTestCase extends BlazeIntegrationTestCase {
+
+  protected EditorTestHelper editorTest;
+
+  @Before
+  public final void doSetup() {
+    BlazeProjectDataManager mockProjectDataManager =
+        new MockBlazeProjectDataManager(MockBlazeProjectDataBuilder.builder(workspaceRoot).build());
+    registerProjectService(BlazeProjectDataManager.class, mockProjectDataManager);
+    editorTest = new EditorTestHelper(getProject(), testFixture);
+
+    // IntelliJ replaces the normal DataManager with a mock version in headless environments.
+    // We rely on a functional DataManager in run configuration tests to recognize when multiple
+    // psi elements are selected.
+    DataManager dataManager =
+        new MyDataManagerImpl() {
+          DataContext dataContext;
+
+          @Override
+          public <T> void saveInDataContext(
+              DataContext dataContext, Key<T> dataKey, @Nullable T data) {
+            this.dataContext = dataContext;
+            super.saveInDataContext(dataContext, dataKey, data);
+          }
+
+          @Override
+          public DataContext getDataContext() {
+            return dataContext != null ? dataContext : super.getDataContext();
+          }
+        };
+    registerApplicationComponent(DataManager.class, dataManager);
+  }
+
+  protected PsiFile createAndIndexFile(WorkspacePath path, String... contents) {
+    PsiFile file = workspace.createPsiFile(path, contents);
+    editorTest.openFileInEditor(file); // open file to trigger update of indices
+    return file;
+  }
+
+  @Nullable
+  protected static String getTestFilterContents(BlazeCommandRunConfiguration config) {
+    BlazeCommandRunConfigurationCommonState handlerState =
+        config.getHandlerStateIfType(BlazeCommandRunConfigurationCommonState.class);
+    return handlerState != null ? handlerState.getTestFilterFlag() : null;
+  }
+
+  @Nullable
+  protected static BlazeCommandName getCommandType(BlazeCommandRunConfiguration config) {
+    BlazeCommandRunConfigurationCommonState handlerState =
+        config.getHandlerStateIfType(BlazeCommandRunConfigurationCommonState.class);
+    return handlerState != null ? handlerState.getCommandState().getCommand() : null;
+  }
+
+  protected ConfigurationContext createContextFromPsi(PsiElement element) {
+    final MapDataContext dataContext = new MapDataContext();
+    dataContext.put(CommonDataKeys.PROJECT, getProject());
+    dataContext.put(LangDataKeys.MODULE, ModuleUtil.findModuleForPsiElement(element));
+    dataContext.put(Location.DATA_KEY, PsiLocation.fromPsiElement(element));
+    return ConfigurationContext.getFromContext(dataContext);
+  }
+
+  protected ConfigurationContext createContextFromMultipleElements(PsiElement[] elements) {
+    final MapDataContext dataContext = new MapDataContext();
+    dataContext.put(CommonDataKeys.PROJECT, getProject());
+    dataContext.put(LangDataKeys.MODULE, ModuleUtil.findModuleForPsiElement(elements[0]));
+    dataContext.put(Location.DATA_KEY, PsiLocation.fromPsiElement(elements[0]));
+    dataContext.put(LangDataKeys.PSI_ELEMENT_ARRAY, elements);
+    return ConfigurationContext.getFromContext(dataContext);
+  }
+
+  @Nullable
+  protected RunConfiguration createConfigurationFromLocation(PsiFile psiFile) {
+    MapDataContext dataContext = new MapDataContext();
+    dataContext.put(CommonDataKeys.PROJECT, getProject());
+    dataContext.put(LangDataKeys.MODULE, ModuleUtil.findModuleForPsiElement(psiFile));
+    dataContext.put(Location.DATA_KEY, PsiLocation.fromPsiElement(psiFile));
+    RunnerAndConfigurationSettings settings =
+        ConfigurationContext.getFromContext(dataContext).getConfiguration();
+    return settings != null ? settings.getConfiguration() : null;
+  }
+
+  protected static ArtifactLocation sourceRoot(String relativePath) {
+    return ArtifactLocation.builder().setRelativePath(relativePath).setIsSource(true).build();
+  }
+}
diff --git a/base/tests/utils/integration/com/google/idea/blaze/base/sync/BlazeSyncIntegrationTestCase.java b/base/tests/utils/integration/com/google/idea/blaze/base/sync/BlazeSyncIntegrationTestCase.java
index 53c6b53..a227d50 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
@@ -27,11 +27,11 @@
 import com.google.common.util.concurrent.ListeningExecutorService;
 import com.google.idea.blaze.base.BlazeIntegrationTestCase;
 import com.google.idea.blaze.base.command.info.BlazeInfo;
+import com.google.idea.blaze.base.command.info.BlazeInfoRunner;
 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;
 import com.google.idea.blaze.base.model.primitives.WorkspaceRoot;
 import com.google.idea.blaze.base.projectview.ProjectViewManager;
@@ -45,16 +45,20 @@
 import com.google.idea.blaze.base.settings.BlazeImportSettings;
 import com.google.idea.blaze.base.settings.BlazeImportSettingsManager;
 import com.google.idea.blaze.base.sync.aspects.BlazeIdeInterface;
+import com.google.idea.blaze.base.sync.aspects.BuildResult;
 import com.google.idea.blaze.base.sync.data.BlazeDataStorage;
 import com.google.idea.blaze.base.sync.projectstructure.ModuleEditorImpl;
 import com.google.idea.blaze.base.sync.projectstructure.ModuleEditorProvider;
+import com.google.idea.blaze.base.sync.projectstructure.ModuleFinder;
 import com.google.idea.blaze.base.sync.projectview.WorkspaceLanguageSettings;
+import com.google.idea.blaze.base.sync.sharding.ShardedTargetList;
 import com.google.idea.blaze.base.sync.workspace.ArtifactLocationDecoder;
 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.google.idea.blaze.base.vcs.BlazeVcsHandler;
 import com.intellij.ide.IdeEventQueue;
+import com.intellij.openapi.module.Module;
 import com.intellij.openapi.project.Project;
 import com.intellij.openapi.roots.ContentEntry;
 import com.intellij.openapi.roots.ModifiableRootModel;
@@ -80,45 +84,61 @@
 
   private MockProjectViewManager projectViewManager;
   private MockBlazeVcsHandler vcsHandler;
-  private MockBlazeInfo blazeInfoData;
+  private MockBlazeInfoRunner blazeInfoData;
   private MockBlazeIdeInterface blazeIdeInterface;
 
   protected ErrorCollector errorCollector;
   protected BlazeContext context;
 
   private ImmutableList<ContentEntry> workspaceContentEntries = ImmutableList.of();
+  private Map<String, ModifiableRootModel> modules = Maps.newHashMap();
+
+  private class MockModuleEditor extends ModuleEditorImpl {
+    public MockModuleEditor(Project project, BlazeImportSettings importSettings) {
+      super(project, importSettings);
+    }
+
+    @Override
+    public void commit() {
+      // don't commit module changes,
+      // and make sure they're properly disposed when the test is finished
+      for (ModifiableRootModel model : modules.values()) {
+        Disposer.register(getTestRootDisposable(), model::dispose);
+        if (model.getModule().getName().equals(BlazeDataStorage.WORKSPACE_MODULE_NAME)) {
+          workspaceContentEntries = ImmutableList.copyOf(model.getContentEntries());
+        }
+      }
+      BlazeSyncIntegrationTestCase.this.modules = modules;
+    }
+  }
+
+  // Since MockModuleEditor does not actually commit modules, the normal ModuleManager
+  // won't find modules we've created. This helps look up modules for later stages of Sync.
+  // We could override ModuleManager, but that has a wide interface and there are a lot of
+  // changes across API versions.
+  private class MockModuleFinder implements ModuleFinder {
+
+    MockModuleFinder() {}
+
+    @Nullable
+    @Override
+    public Module findModuleByName(String name) {
+      return getModuleCreatedDuringSync(name);
+    }
+  }
 
   @Before
   public void doSetup() throws Exception {
     projectViewManager = new MockProjectViewManager();
     vcsHandler = new MockBlazeVcsHandler();
-    blazeInfoData = new MockBlazeInfo();
+    blazeInfoData = new MockBlazeInfoRunner();
     blazeIdeInterface = new MockBlazeIdeInterface();
     registerProjectService(ProjectViewManager.class, projectViewManager);
     registerExtension(BlazeVcsHandler.EP_NAME, vcsHandler);
-    registerApplicationService(BlazeInfo.class, blazeInfoData);
+    registerApplicationService(BlazeInfoRunner.class, blazeInfoData);
     registerApplicationService(BlazeIdeInterface.class, blazeIdeInterface);
-    registerApplicationService(
-        ModuleEditorProvider.class,
-        new ModuleEditorProvider() {
-          @Override
-          public ModuleEditorImpl getModuleEditor(
-              Project project, BlazeImportSettings importSettings) {
-            return new ModuleEditorImpl(project, importSettings) {
-              @Override
-              public void commit() {
-                // don't commit module changes,
-                // and make sure they're properly disposed when the test is finished
-                for (ModifiableRootModel model : modules.values()) {
-                  Disposer.register(getTestRootDisposable(), model::dispose);
-                  if (model.getModule().getName().equals(BlazeDataStorage.WORKSPACE_MODULE_NAME)) {
-                    workspaceContentEntries = ImmutableList.copyOf(model.getContentEntries());
-                  }
-                }
-              }
-            };
-          }
-        });
+    registerApplicationService(ModuleEditorProvider.class, MockModuleEditor::new);
+    registerProjectService(ModuleFinder.class, new MockModuleFinder());
 
     errorCollector = new ErrorCollector();
     context = new BlazeContext();
@@ -145,6 +165,12 @@
     return workspaceContentEntries;
   }
 
+  /** The modules created during sync */
+  private Module getModuleCreatedDuringSync(String module) {
+    ModifiableRootModel modifiableRootModel = modules.get(module);
+    return modifiableRootModel != null ? modifiableRootModel.getModule() : null;
+  }
+
   /** Search the workspace module's {@link ContentEntry}s for one with the given file. */
   @Nullable
   protected ContentEntry findContentEntry(VirtualFile root) {
@@ -262,13 +288,13 @@
     }
   }
 
-  private static class MockBlazeInfo extends BlazeInfo {
+  private static class MockBlazeInfoRunner extends BlazeInfoRunner {
     private final Map<String, String> results = Maps.newHashMap();
 
     @Override
     public ListenableFuture<String> runBlazeInfo(
         @Nullable BlazeContext context,
-        BuildSystem buildSystem,
+        String binaryPath,
         WorkspaceRoot workspaceRoot,
         List<String> blazeFlags,
         String key) {
@@ -278,7 +304,7 @@
     @Override
     public ListenableFuture<byte[]> runBlazeInfoGetBytes(
         @Nullable BlazeContext context,
-        BuildSystem buildSystem,
+        String binaryPath,
         WorkspaceRoot workspaceRoot,
         List<String> blazeFlags,
         String key) {
@@ -286,12 +312,13 @@
     }
 
     @Override
-    public ListenableFuture<ImmutableMap<String, String>> runBlazeInfo(
+    public ListenableFuture<BlazeInfo> runBlazeInfo(
         @Nullable BlazeContext context,
         BuildSystem buildSystem,
+        String binaryPath,
         WorkspaceRoot workspaceRoot,
         List<String> blazeFlags) {
-      return Futures.immediateFuture(ImmutableMap.copyOf(results));
+      return Futures.immediateFuture(new BlazeInfo(buildSystem, ImmutableMap.copyOf(results)));
     }
 
     public void setResults(Map<String, String> results) {
@@ -310,7 +337,7 @@
         WorkspaceRoot workspaceRoot,
         ProjectViewSet projectViewSet,
         BlazeVersionData blazeVersionData,
-        List<TargetExpression> targets,
+        ShardedTargetList shardedTargets,
         WorkspaceLanguageSettings workspaceLanguageSettings,
         ArtifactLocationDecoder artifactLocationDecoder,
         SyncState.Builder syncStateBuilder,
@@ -326,7 +353,7 @@
         WorkspaceRoot workspaceRoot,
         ProjectViewSet projectViewSet,
         BlazeVersionData blazeVersionData,
-        List<TargetExpression> targets) {
+        ShardedTargetList shardedTargets) {
       return BuildResult.SUCCESS;
     }
 
@@ -337,7 +364,7 @@
         WorkspaceRoot workspaceRoot,
         ProjectViewSet projectViewSet,
         BlazeVersionData blazeVersionData,
-        List<TargetExpression> targets) {
+        ShardedTargetList shardedTargets) {
       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 874fd28..020ee9f 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
@@ -69,7 +69,7 @@
 
   protected Project project;
   private ExtensionsAreaImpl extensionsArea;
-  private Disposable testDisposable;
+  protected Disposable testDisposable;
 
   private static class RootDisposable implements Disposable {
     @Override
diff --git a/base/tests/utils/unit/com/google/idea/blaze/base/TestUtils.java b/base/tests/utils/unit/com/google/idea/blaze/base/TestUtils.java
index f040cf2..02aaf29 100644
--- a/base/tests/utils/unit/com/google/idea/blaze/base/TestUtils.java
+++ b/base/tests/utils/unit/com/google/idea/blaze/base/TestUtils.java
@@ -17,7 +17,6 @@
 
 import static org.junit.Assert.fail;
 
-import com.google.common.util.concurrent.ListeningExecutorService;
 import com.google.common.util.concurrent.MoreExecutors;
 import com.intellij.mock.MockApplicationEx;
 import com.intellij.mock.MockProject;
@@ -29,6 +28,7 @@
 import com.intellij.openapi.util.Disposer;
 import com.intellij.openapi.vfs.encoding.EncodingManager;
 import com.intellij.openapi.vfs.encoding.EncodingManagerImpl;
+import com.intellij.util.PlatformUtils;
 import com.intellij.util.pico.DefaultPicoContainer;
 import java.io.ByteArrayOutputStream;
 import java.io.IOException;
@@ -36,16 +36,17 @@
 import java.io.ObjectOutputStream;
 import java.io.Serializable;
 import java.util.concurrent.Callable;
+import java.util.concurrent.ExecutorService;
 import java.util.concurrent.Future;
+import javax.annotation.Nullable;
 import org.jetbrains.annotations.NotNull;
-import org.jetbrains.annotations.Nullable;
 import org.picocontainer.PicoContainer;
 
 /** Test utilities. */
 public class TestUtils {
 
   static class BlazeMockApplication extends MockApplicationEx {
-    private final ListeningExecutorService executor = MoreExecutors.sameThreadExecutor();
+    private final ExecutorService executor = MoreExecutors.newDirectExecutorService();
 
     public BlazeMockApplication(@NotNull Disposable parentDisposable) {
       super(parentDisposable);
@@ -119,4 +120,22 @@
       }
     }
   }
+
+  /**
+   * Sets the platform prefix system property, reverting to the previous value when the supplied
+   * parent disposable is disposed.
+   */
+  public static void setPlatformPrefix(Disposable parentDisposable, String platformPrefix) {
+    String prevValue = System.getProperty(PlatformUtils.PLATFORM_PREFIX_KEY);
+    System.setProperty(PlatformUtils.PLATFORM_PREFIX_KEY, platformPrefix);
+    Disposer.register(
+        parentDisposable,
+        () -> {
+          if (prevValue != null) {
+            System.setProperty(PlatformUtils.PLATFORM_PREFIX_KEY, prevValue);
+          } else {
+            System.clearProperty(PlatformUtils.PLATFORM_PREFIX_KEY);
+          }
+        });
+  }
 }
diff --git a/base/tests/utils/unit/com/google/idea/blaze/base/async/executor/MockBlazeExecutor.java b/base/tests/utils/unit/com/google/idea/blaze/base/async/executor/MockBlazeExecutor.java
index 3e970b4..34caa8b 100644
--- a/base/tests/utils/unit/com/google/idea/blaze/base/async/executor/MockBlazeExecutor.java
+++ b/base/tests/utils/unit/com/google/idea/blaze/base/async/executor/MockBlazeExecutor.java
@@ -23,7 +23,7 @@
 /** Used in tests. */
 public class MockBlazeExecutor extends BlazeExecutor {
 
-  private final ListeningExecutorService executor = MoreExecutors.sameThreadExecutor();
+  private final ListeningExecutorService executor = MoreExecutors.newDirectExecutorService();
 
   @Override
   public <T> ListenableFuture<T> submit(final Callable<T> callable) {
diff --git a/base/tests/utils/unit/com/google/idea/blaze/base/model/MockBlazeProjectDataBuilder.java b/base/tests/utils/unit/com/google/idea/blaze/base/model/MockBlazeProjectDataBuilder.java
index 25bd08d..8e104a7 100644
--- a/base/tests/utils/unit/com/google/idea/blaze/base/model/MockBlazeProjectDataBuilder.java
+++ b/base/tests/utils/unit/com/google/idea/blaze/base/model/MockBlazeProjectDataBuilder.java
@@ -15,26 +15,24 @@
  */
 package com.google.idea.blaze.base.model;
 
-import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableMap;
 import com.google.common.collect.ImmutableMultimap;
 import com.google.common.collect.ImmutableSet;
+import com.google.idea.blaze.base.command.info.BlazeInfo;
 import com.google.idea.blaze.base.ideinfo.TargetKey;
 import com.google.idea.blaze.base.ideinfo.TargetMap;
-import com.google.idea.blaze.base.model.primitives.ExecutionRootPath;
 import com.google.idea.blaze.base.model.primitives.WorkspaceRoot;
 import com.google.idea.blaze.base.model.primitives.WorkspaceType;
 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.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;
 
 /**
- * Use to build mock praject data for tests.
+ * Use to build mock project data for tests.
  *
- * <p>For any data you don't supply, the builder make a best-effort attempt to create default
+ * <p>For any data you don't supply, the builder makes a best-effort attempt to create default
  * objects using whatever data you have supplied if applicable.
  */
 public class MockBlazeProjectDataBuilder {
@@ -42,8 +40,8 @@
 
   private long syncTime = 0;
   private TargetMap targetMap;
-  private ImmutableMap<String, String> blazeInfo;
-  private BlazeRoots blazeRoots;
+  private String outputBase;
+  private BlazeInfo blazeInfo;
   private BlazeVersionData blazeVersionData;
   private WorkspacePathResolver workspacePathResolver;
   private ArtifactLocationDecoder artifactLocationDecoder;
@@ -51,7 +49,7 @@
   private SyncState syncState;
   private ImmutableMultimap<TargetKey, TargetKey> reverseDependencies;
 
-  public MockBlazeProjectDataBuilder(WorkspaceRoot workspaceRoot) {
+  private MockBlazeProjectDataBuilder(WorkspaceRoot workspaceRoot) {
     this.workspaceRoot = workspaceRoot;
   }
 
@@ -69,13 +67,13 @@
     return this;
   }
 
-  public MockBlazeProjectDataBuilder setBlazeInfo(ImmutableMap<String, String> blazeInfo) {
-    this.blazeInfo = blazeInfo;
+  public MockBlazeProjectDataBuilder setOutputBase(String outputBase) {
+    this.outputBase = outputBase;
     return this;
   }
 
-  public MockBlazeProjectDataBuilder setBlazeRoots(BlazeRoots blazeRoots) {
-    this.blazeRoots = blazeRoots;
+  public MockBlazeProjectDataBuilder setBlazeInfo(BlazeInfo blazeInfo) {
+    this.blazeInfo = blazeInfo;
     return this;
   }
 
@@ -116,27 +114,26 @@
   public BlazeProjectData build() {
     TargetMap targetMap =
         this.targetMap != null ? this.targetMap : new TargetMap(ImmutableMap.of());
-    ImmutableMap<String, String> blazeInfo =
-        this.blazeInfo != null ? this.blazeInfo : ImmutableMap.of();
-    BlazeRoots blazeRoots =
-        this.blazeRoots != null
-            ? this.blazeRoots
-            : new BlazeRoots(
-                null,
-                ImmutableList.of(workspaceRoot.directory()),
-                new ExecutionRootPath("bin"),
-                new ExecutionRootPath("gen"),
-                null);
+    BlazeInfo blazeInfo = this.blazeInfo;
+    if (blazeInfo == null) {
+      String outputBase = this.outputBase != null ? this.outputBase : "/usr/workspace/1234";
+      blazeInfo =
+          BlazeInfo.createMockBlazeInfo(
+              outputBase,
+              outputBase + "/execroot",
+              outputBase + "/execroot/bin",
+              outputBase + "/execroot/gen");
+    }
     BlazeVersionData blazeVersionData =
         this.blazeVersionData != null ? this.blazeVersionData : new BlazeVersionData();
     WorkspacePathResolver workspacePathResolver =
         this.workspacePathResolver != null
             ? this.workspacePathResolver
-            : new WorkspacePathResolverImpl(workspaceRoot, blazeRoots);
+            : new WorkspacePathResolverImpl(workspaceRoot);
     ArtifactLocationDecoder artifactLocationDecoder =
         this.artifactLocationDecoder != null
             ? this.artifactLocationDecoder
-            : new ArtifactLocationDecoderImpl(blazeRoots, workspacePathResolver);
+            : new ArtifactLocationDecoderImpl(blazeInfo, workspacePathResolver);
     WorkspaceLanguageSettings workspaceLanguageSettings =
         this.workspaceLanguageSettings != null
             ? this.workspaceLanguageSettings
@@ -150,13 +147,11 @@
         syncTime,
         targetMap,
         blazeInfo,
-        blazeRoots,
         blazeVersionData,
         workspacePathResolver,
         artifactLocationDecoder,
         workspaceLanguageSettings,
         syncState,
-        reverseDependencies,
-        null);
+        reverseDependencies);
   }
 }
diff --git a/base/tests/utils/unit/com/google/idea/blaze/base/prefetch/MockPrefetchService.java b/base/tests/utils/unit/com/google/idea/blaze/base/prefetch/MockPrefetchService.java
index 8dbac18..bedd5d9 100644
--- a/base/tests/utils/unit/com/google/idea/blaze/base/prefetch/MockPrefetchService.java
+++ b/base/tests/utils/unit/com/google/idea/blaze/base/prefetch/MockPrefetchService.java
@@ -18,6 +18,7 @@
 import com.google.common.util.concurrent.Futures;
 import com.google.common.util.concurrent.ListenableFuture;
 import com.google.idea.blaze.base.model.BlazeProjectData;
+import com.google.idea.blaze.base.projectview.ProjectViewSet;
 import com.intellij.openapi.project.Project;
 import java.io.File;
 import java.util.Collection;
@@ -32,7 +33,7 @@
 
   @Override
   public ListenableFuture<?> prefetchProjectFiles(
-      Project project, BlazeProjectData blazeProjectData) {
+      Project project, ProjectViewSet projectViewSet, BlazeProjectData blazeProjectData) {
     return Futures.immediateFuture(null);
   }
 }
diff --git a/build_defs/BUILD b/build_defs/BUILD
index 3c8b0b3..19ad33f 100644
--- a/build_defs/BUILD
+++ b/build_defs/BUILD
@@ -25,3 +25,13 @@
     name = "api_version_txt",
     srcs = ["api_version_txt.py"],
 )
+
+py_binary(
+    name = "append_optional_xml_elements",
+    srcs = ["append_optional_xml_elements.py"],
+)
+
+py_binary(
+    name = "package_meta_inf_files",
+    srcs = ["package_meta_inf_files.py"],
+)
diff --git a/build_defs/append_optional_xml_elements.py b/build_defs/append_optional_xml_elements.py
new file mode 100755
index 0000000..c987fe1
--- /dev/null
+++ b/build_defs/append_optional_xml_elements.py
@@ -0,0 +1,46 @@
+"""Appends XML elements specifying optional dependencies to a plugin XML file.
+"""
+
+import argparse
+from itertools import izip
+from xml.dom.minidom import parse
+
+parser = argparse.ArgumentParser()
+
+parser.add_argument(
+    "--plugin_xml", help="The main plugin xml file", required=True)
+parser.add_argument("--output", help="The output file.")
+parser.add_argument(
+    "optional_xml_files",
+    nargs="+",
+    help="Sequence of module, module xml... pairs")
+
+
+def pairwise(t):
+  it = iter(t)
+  return izip(it, it)
+
+
+def main():
+
+  args = parser.parse_args()
+  dom = parse(args.plugin_xml)
+
+  plugin_xml = dom.documentElement
+
+  for module, optional_xml in pairwise(args.optional_xml_files):
+    depends_element = dom.createElement("depends")
+    depends_element.setAttribute("optional", "true")
+    depends_element.setAttribute("config-file", optional_xml)
+    depends_element.appendChild(dom.createTextNode(module))
+    plugin_xml.appendChild(depends_element)
+
+  if args.output:
+    with file(args.output, "w") as f:
+      f.write(dom.toxml())
+  else:
+    print dom.toxml()
+
+
+if __name__ == "__main__":
+  main()
diff --git a/build_defs/build_defs.bzl b/build_defs/build_defs.bzl
index b695241..5325028 100644
--- a/build_defs/build_defs.bzl
+++ b/build_defs/build_defs.bzl
@@ -2,6 +2,7 @@
 """
 
 load(":intellij_plugin_debug_target.bzl", "intellij_plugin_debug_target")
+load(":intellij_plugin.bzl", "intellij_plugin", "optional_plugin_xml")
 
 def merged_plugin_xml(name, srcs, **kwargs):
   """Merges N plugin.xml files together."""
@@ -178,72 +179,7 @@
       tools = [api_version_txt_tool],
       **kwargs)
 
-def intellij_plugin(name, deps, plugin_xml, meta_inf_files=[], jar_name=None, **kwargs):
-  """Creates an intellij plugin from the given deps and plugin.xml.
-
-  Args:
-    name: The name of the target
-    deps: Any java dependencies rolled up into the plugin jar.
-    plugin_xml: An xml file to be placed in META-INF/plugin.jar
-    meta_inf_files: Any further files to be placed in META-INF/plugin.jar
-    jar_name: The name of the final plugin jar, or <name>.jar if None
-    **kwargs: Any further arguments to be passed to the final target
-  """
-  zip_tool = "//third_party:zip"
-  binary_name = name + "_binary"
-  deploy_jar = binary_name + "_deploy.jar"
-  native.java_binary(
-      name = binary_name,
-      runtime_deps = deps,
-      create_executable = 0,
-  )
-  cmd = [
-      "cp $(location {deploy_jar}) $@".format(deploy_jar=deploy_jar),
-      "chmod +w $@",
-      "mkdir -p META-INF",
-      "cp $(location {plugin_xml}) META-INF/plugin.xml".format(plugin_xml=plugin_xml),
-  ]
-  srcs = meta_inf_files + [
-      plugin_xml,
-      deploy_jar,
-  ]
-
-  for meta_inf_file in meta_inf_files:
-    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))
-  cmd.append("rm -rf META-INF")
-
-  jar_name = jar_name or (name + ".jar")
-  native.genrule(
-      name = name + "_genrule",
-      srcs = srcs,
-      tools = [zip_tool],
-      outs = [jar_name],
-      cmd = " ; ".join(cmd),
-  )
-
-  # included (with tag) as a hack so that IJwB can recognize this is an intellij plugin
-  native.java_import(
-      name = name,
-      jars = [name + ".jar"],
-      tags = ["intellij-plugin"],
-      **kwargs)
-
-def plugin_bundle(name, plugins):
-  """Communicates to IJwB a set of plugins which should be loaded together in a run configuration.
-
-  Args:
-    name: the name of this target
-    plugins: the 'intellij_plugin' targets to be bundled
-  """
-  native.java_library(
-      name = name,
-      exports = plugins,
-      tags = ["intellij-plugin-bundle"],
-  )
-
-def repackaged_jar(name, deps, rules, launcher=None, **kwargs):
+def repackaged_jar(name, deps, rules, **kwargs):
   """Repackages classes in a jar, to avoid collisions in the classpath.
 
   Args:
@@ -255,25 +191,18 @@
           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"
+  java_binary_name = name + "_orig"
   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)
+      runtime_deps = deps)
+  _repackaged_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):
+def _repackaged_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)
@@ -290,7 +219,7 @@
       repackage_tool = repackage_tool,
       deploy_jar = deploy_jar,
   ))
-  genrule_name = name + "_gen"
+  genrule_name = name + "_repackaged"
   native.genrule(
       name = genrule_name,
       srcs = [deploy_jar],
@@ -302,3 +231,9 @@
       name = name,
       jars = [out],
       **kwargs)
+
+def beta_gensignature(name, srcs, stable, stable_version, beta_version):
+  if stable_version == beta_version:
+    native.alias(name = name, actual = stable)
+  else:
+    native.gensignature(name = name, srcs = srcs)
diff --git a/build_defs/intellij_plugin.bzl b/build_defs/intellij_plugin.bzl
new file mode 100644
index 0000000..207435d
--- /dev/null
+++ b/build_defs/intellij_plugin.bzl
@@ -0,0 +1,220 @@
+"""IntelliJ plugin target rule.
+
+Creates a plugin jar with the given plugin xml and any
+optional plugin xmls.
+
+To provide optional plugin xmls, use the 'optional_plugin_xml'
+rule. These will be renamed, put in the META-INF directory,
+and the main plugin xml stamped with optional plugin dependencies
+that point to the correct META-INF optional plugin xmls.
+
+optional_plugin_xml(
+  name = "optional_python_xml",
+  plugin_xml = "my_optional_python_plugin.xml",
+  module = "com.idea.python.module.id",
+)
+
+intellij_plugin(
+  name = "my_plugin",
+  plugin_xml = ["my_plugin.xml"],
+  optional_plugin_xmls = [":optional_python_xml"],
+  deps = [
+    ":code_deps",
+  ],
+)
+
+"""
+
+optional_plugin_xml_provider = provider()
+
+def _optional_plugin_xml_impl(ctx):
+  attr = ctx.attr
+  optional_plugin_xmls = []
+  if ctx.file.plugin_xml:
+    optional_plugin_xmls.append(struct(
+        plugin_xml = ctx.file.plugin_xml,
+        module = attr.module,
+    ))
+  return struct(
+      optional_plugin_xml_data = optional_plugin_xml_provider(
+          optional_plugin_xmls = optional_plugin_xmls,
+      ),
+  )
+
+optional_plugin_xml = rule(
+    implementation = _optional_plugin_xml_impl,
+    attrs = {
+        "plugin_xml": attr.label(mandatory=True, allow_single_file=[".xml"]),
+        "module": attr.string(mandatory=True),
+    },
+)
+
+def _merge_optional_plugin_xmls(ctx):
+  # Collect optional plugin xmls
+  module_to_xmls = {}
+  for target in ctx.attr.optional_plugin_xmls:
+    if not hasattr(target, "optional_plugin_xml_data"):
+      fail("optional_plugin_xmls only accepts optional_plugin_xml targets")
+    for xml in target.optional_plugin_xml_data.optional_plugin_xmls:
+      module = xml.module
+      plugin_xmls = module_to_xmls.setdefault(module, [])
+      plugin_xmls.append(xml.plugin_xml)
+
+  # Merge xmls with the same module dependency
+  module_to_merged_xmls = {}
+  for module, plugin_xmls in module_to_xmls.items():
+    merged_name = "merged_xml_for_" + module + "_" + ctx.label.name + ".xml"
+    merged_file = ctx.new_file(merged_name)
+    ctx.action(
+        executable = ctx.executable._merge_xml_binary,
+        arguments = ["--output", merged_file.path] + [plugin_xml.path for plugin_xml in plugin_xmls],
+        inputs = list(plugin_xmls),
+        outputs = [merged_file],
+        progress_message = "Merging optional xmls",
+        mnemonic = "MergeOptionalXmls",
+    )
+    module_to_merged_xmls[module] = merged_file
+  return module_to_merged_xmls
+
+def _add_optional_dependencies_to_plugin_xml(ctx, modules):
+  input_plugin_xml_file = ctx.file.plugin_xml
+  if not modules:
+    return input_plugin_xml_file
+
+  # Add optional dependencies into the plugin xml
+  args = []
+  final_plugin_xml_file = ctx.new_file("final_plugin_xml_" + ctx.label.name + ".xml")
+  args.extend(["--plugin_xml", input_plugin_xml_file.path])
+  args.extend(["--output", final_plugin_xml_file.path])
+  for module in modules:
+    args.append(module)
+    args.append(_filename_for_module_dependency(module))
+  ctx.action(
+      executable = ctx.executable._append_optional_xml_elements,
+      arguments = args,
+      inputs = [input_plugin_xml_file],
+      outputs = [final_plugin_xml_file],
+      progress_message = "Adding optional dependencies to final plugin xml",
+      mnemonic = "AddModuleDependencies",
+  )
+  return final_plugin_xml_file
+
+def _only_file(target):
+  return list(target.files)[0]
+
+def _filename_for_module_dependency(module):
+  """A unique filename for the optional xml dependency for a given module."""
+  return "optional-" + module + ".xml"
+
+def _package_meta_inf_files(ctx, final_plugin_xml_file, module_to_merged_xmls):
+  jar_name = ctx.attr.jar_name
+  jar_file = ctx.new_file(jar_name)
+
+  args = []
+  args.extend(["--deploy_jar", ctx.file.deploy_jar.path])
+  args.extend(["--output", jar_file.path])
+  args.extend([final_plugin_xml_file.path, "plugin.xml"])
+  for module, merged_xml in module_to_merged_xmls.items():
+    args.append(merged_xml.path)
+    args.append(_filename_for_module_dependency(module))
+  ctx.action(
+      executable = ctx.executable._package_meta_inf_files,
+      arguments = args,
+      inputs = [ctx.file.deploy_jar, final_plugin_xml_file] + module_to_merged_xmls.values(),
+      outputs = [jar_file],
+      mnemonic = "PackagePluginJar",
+      progress_message = "Packaging plugin jar",
+  )
+  return jar_file
+
+def _intellij_plugin_jar_impl(ctx):
+  module_to_merged_xmls = _merge_optional_plugin_xmls(ctx)
+  final_plugin_xml_file = _add_optional_dependencies_to_plugin_xml(ctx, module_to_merged_xmls.keys())
+  jar_file = _package_meta_inf_files(ctx, final_plugin_xml_file, module_to_merged_xmls)
+  files = set([jar_file])
+  return struct(
+      files = files,
+  )
+
+_intellij_plugin_jar = rule(
+    implementation = _intellij_plugin_jar_impl,
+    attrs = {
+        "deploy_jar": attr.label(mandatory=True, allow_single_file=[".jar"]),
+        "plugin_xml": attr.label(mandatory=True, allow_single_file=[".xml"]),
+        "optional_plugin_xmls": attr.label_list(),
+        "jar_name": attr.string(mandatory=True),
+        "_merge_xml_binary": attr.label(
+            default = Label("//build_defs:merge_xml"),
+            executable = True,
+            cfg = "host",
+        ),
+        "_append_optional_xml_elements": attr.label(
+            default = Label("//build_defs:append_optional_xml_elements"),
+            executable = True,
+            cfg = "host",
+        ),
+        "_package_meta_inf_files": attr.label(
+            default = Label("//build_defs:package_meta_inf_files"),
+            executable = True,
+            cfg = "host",
+        ),
+    },
+)
+
+def intellij_plugin(name, deps, plugin_xml, optional_plugin_xmls=[], jar_name=None, **kwargs):
+  """Creates an intellij plugin from the given deps and plugin.xml.
+
+  Args:
+    name: The name of the target
+    deps: Any java dependencies rolled up into the plugin jar.
+    plugin_xml: An xml file to be placed in META-INF/plugin.jar
+    optional_plugin_xmls: A list of optional_plugin_xml targets.
+    jar_name: The name of the final plugin jar, or <name>.jar if None
+    **kwargs: Any further arguments to be passed to the final target
+  """
+  binary_name = name + "_binary"
+  deploy_jar = binary_name + "_deploy.jar"
+  native.java_binary(
+      name = binary_name,
+      runtime_deps = deps,
+      create_executable = 0,
+  )
+  jar_target_name =  name + "_intellij_plugin_jar"
+  _intellij_plugin_jar(
+      name = jar_target_name,
+      deploy_jar = deploy_jar,
+      jar_name = jar_name or (name + ".jar"),
+      plugin_xml = plugin_xml,
+      optional_plugin_xmls = optional_plugin_xmls,
+  )
+  # included (with tag) as a hack so that IJwB can recognize this is an intellij plugin
+  native.java_import(
+      name = name,
+      jars = [jar_target_name],
+      tags = ["intellij-plugin"],
+      **kwargs)
+
+def _append_optional_dependencies(name, plugin_xml, module_to_merged_xml):
+  """Appends optional dependency xml elements to plugin xml."""
+  append_elements_tool = "//build_defs:append_optional_xml_elements"
+  args = [
+      "./$(location {append_elements_tool})",
+      "--plugin_xml=$(location {plugin_xml})",
+      "--optional_xml_files={merged_optional_xml_files}",
+  ]
+  dictionary = {k: _filename_for_module_dependency(k) for k in module_to_merged_xml.keys()}
+  cmd = " ".join(args).format(
+      append_elements_tool=append_elements_tool,
+      plugin_xml=plugin_xml,
+      merged_optional_xml_files='"%s"' % str(dictionary).replace('"', '\\"'),
+  ) + "> $@"
+
+  srcs = module_to_merged_xml.values() + [plugin_xml]
+
+  native.genrule(
+      name = name,
+      srcs = srcs,
+      outs = [name + ".xml"],
+      cmd = cmd,
+      tools = [append_elements_tool],
+  )
diff --git a/build_defs/intellij_plugin_debug_target.bzl b/build_defs/intellij_plugin_debug_target.bzl
index 484a6fe..37ec6f6 100644
--- a/build_defs/intellij_plugin_debug_target.bzl
+++ b/build_defs/intellij_plugin_debug_target.bzl
@@ -28,6 +28,8 @@
 
 """
 
+SUFFIX = ".intellij-plugin-debug-target-deploy-info"
+
 def _trim_start(path, prefix):
   return path[len(prefix):] if path.startswith(prefix) else path
 
@@ -86,12 +88,29 @@
     implementation = _intellij_plugin_debug_target_aspect_impl,
 )
 
+def _build_deploy_info_file(deploy_file):
+  return struct(
+      execution_path = deploy_file.src.path,
+      deploy_location = deploy_file.deploy_location,
+  )
+
 def _intellij_plugin_debug_target_impl(ctx):
   files = set()
   deploy_files = []
   for target in ctx.attr.deps:
     files = files | target.files
     deploy_files.extend(target.aspect_intellij_plugin_deploy_info.deploy_files)
+  deploy_info = struct(
+      deploy_files = [_build_deploy_info_file(f) for f in deploy_files]
+  )
+  output = ctx.new_file(ctx.label.name + SUFFIX)
+  ctx.file_action(output, deploy_info.to_proto())
+
+  # We've already consumed any dependent intellij_plugin_debug_targets into our own,
+  # do not build or report these
+  files = set([f for f in files if not f.path.endswith(SUFFIX)])
+  files = files | set([output])
+
   return struct(
       files = files,
       intellij_plugin_deploy_info = struct(
diff --git a/build_defs/merge_xml.py b/build_defs/merge_xml.py
index b5840e6..2c58164 100755
--- a/build_defs/merge_xml.py
+++ b/build_defs/merge_xml.py
@@ -1,9 +1,22 @@
 """Merges multiple xml files with the same top element tags into a single file.
 """
 
+import argparse
 import sys
 from xml.dom.minidom import parse
 
+parser = argparse.ArgumentParser()
+
+parser.add_argument(
+    "--output",
+    help="The file to output to. If none, prints to stdout.",
+    required=False,)
+
+parser.add_argument(
+    "xmls",
+    nargs="+",
+    help="The xml files to merge",)
+
 
 def AppendFileToTree(filepath, tree):
   """Reads XML from a file and appends XML content to the tree.
@@ -30,12 +43,16 @@
 
 
 if __name__ == "__main__":
-  if len(sys.argv) < 2:
-    print "Need xml filename(s) to be checked as parameter"
+  args = parser.parse_args()
+  if not args.xmls:
     sys.exit(2)
 
-  dom = parse(sys.argv[1])
-  for filename in sys.argv[2:]:
+  dom = parse(args.xmls[0])
+  for filename in args.xmls[1:]:
     AppendFileToTree(filename, dom)
 
-  print dom.toxml()
+  if args.output:
+    with file(args.output, "w") as f:
+      f.write(dom.toxml())
+  else:
+    print dom.toxml()
diff --git a/build_defs/package_meta_inf_files.py b/build_defs/package_meta_inf_files.py
new file mode 100644
index 0000000..c261974
--- /dev/null
+++ b/build_defs/package_meta_inf_files.py
@@ -0,0 +1,44 @@
+"""Adds a list of files into the META-INF directory of the passed deploy jar.
+"""
+
+import argparse
+from itertools import izip
+import shutil
+import zipfile
+
+# Set to Jan 1 1980, the earliest date supported by zipfile
+ZIP_DATE = (1980, 1, 1, 0, 0, 0)
+
+parser = argparse.ArgumentParser()
+
+parser.add_argument(
+    "--deploy_jar",
+    required=True,
+    help="The deploy jar to modify.",)
+parser.add_argument(
+    "--output",
+    required=True,
+    help="The output file.",)
+parser.add_argument(
+    "meta_inf_files",
+    nargs="+",
+    help="Sequence of input file, final file name pairs",)
+
+
+def pairwise(t):
+  it = iter(t)
+  return izip(it, it)
+
+
+def main():
+  args = parser.parse_args()
+
+  shutil.copyfile(args.deploy_jar, args.output)
+  output_jar = zipfile.ZipFile(args.output, "a")
+  for meta_inf_file, name in pairwise(args.meta_inf_files):
+    with file(meta_inf_file) as f:
+      zip_info = zipfile.ZipInfo("META-INF/" + name, ZIP_DATE)
+      output_jar.writestr(zip_info, f.read())
+
+if __name__ == "__main__":
+  main()
diff --git a/clwb/BUILD b/clwb/BUILD
index f1048b9..e420e93 100644
--- a/clwb/BUILD
+++ b/clwb/BUILD
@@ -10,6 +10,7 @@
     "merged_plugin_xml",
     "stamped_plugin_xml",
 )
+load("//intellij_platform_sdk:build_defs.bzl", "select_for_plugin_api")
 load("//:version.bzl", "VERSION")
 
 merged_plugin_xml(
@@ -18,8 +19,8 @@
         "src/META-INF/clwb.xml",
         "//base:plugin_xml",
         "//cpp:plugin_xml",
+        "//python:plugin_xml",
     ],
-    visibility = ["//visibility:public"],
 )
 
 merged_plugin_xml(
@@ -32,6 +33,7 @@
 
 stamped_plugin_xml(
     name = "stamped_plugin_xml",
+    changelog_file = "//:changelog",
     include_product_code_in_stamp = True,
     plugin_id = "com.google.idea.bazel.clwb",
     plugin_name = "CLion with Bazel",
@@ -43,18 +45,30 @@
 java_library(
     name = "clwb_lib",
     srcs = glob(["src/**/*.java"]),
-    visibility = ["//visibility:public"],
+    runtime_deps = [
+        "//terminal",
+    ] + select_for_plugin_api({
+        "clion-2016.3.2": [],
+        "default": ["//python"],
+    }),
     deps = [
         "//base",
         "//common/experiments",
         "//cpp",
         "//intellij_platform_sdk:plugin_api",
+        "//sdkcompat",
         "@jsr305_annotations//jar",
     ],
 )
 
+OPTIONAL_PLUGIN_XMLS = [
+    "//python:optional_xml",
+    "//terminal:optional_xml",
+]
+
 intellij_plugin(
     name = "clwb_bazel",
+    optional_plugin_xmls = OPTIONAL_PLUGIN_XMLS,
     plugin_xml = ":stamped_plugin_xml",
     deps = [
         ":clwb_lib",
diff --git a/clwb/clwb.bazelproject b/clwb/clwb.bazelproject
index 76911c2..b083707 100644
--- a/clwb/clwb.bazelproject
+++ b/clwb/clwb.bazelproject
@@ -3,7 +3,6 @@
   -ijwb
   -aswb
   -plugin_dev
-  -cpp/src/com/google/idea/blaze/cpp/versioned/v145
 
 targets:
   //clwb:clwb_bazel
diff --git a/clwb/src/META-INF/clwb.xml b/clwb/src/META-INF/clwb.xml
index 222ef38..1b79d3c 100644
--- a/clwb/src/META-INF/clwb.xml
+++ b/clwb/src/META-INF/clwb.xml
@@ -50,6 +50,7 @@
     <SyncPlugin implementation="com.google.idea.blaze.clwb.sync.BlazeCLionSyncPlugin"/>
     <BlazeCommandRunConfigurationHandlerProvider implementation="com.google.idea.blaze.clwb.run.BlazeCidrRunConfigurationHandlerProvider" order="first"/>
     <RunConfigurationFactory implementation="com.google.idea.blaze.clwb.run.BlazeCidrDebuggableConfigurationFactory"/>
+    <BlazeTestEventsHandler implementation="com.google.idea.blaze.clwb.run.test.BlazeCidrTestEventsHandler"/>
   </extensions>
 
   <actions>
@@ -72,6 +73,11 @@
     <component>
       <implementation-class>com.google.idea.blaze.clwb.run.producers.NonBlazeProducerSuppressor</implementation-class>
     </component>
+    <component>
+      <interface-class>com.jetbrains.cidr.cpp.cmake.workspace.CMakeWorkspace</interface-class>
+      <implementation-class>com.google.idea.blaze.plugin.CMakeWorkspaceOverride</implementation-class>
+      <option name="overrides" value="true"/>
+    </component>
   </project-components>
 
 </idea-plugin>
diff --git a/clwb/src/com/google/idea/blaze/clwb/problemsview/BlazeProblemsViewConsole.java b/clwb/src/com/google/idea/blaze/clwb/problemsview/BlazeProblemsViewConsole.java
index 543f346..725dde4 100644
--- a/clwb/src/com/google/idea/blaze/clwb/problemsview/BlazeProblemsViewConsole.java
+++ b/clwb/src/com/google/idea/blaze/clwb/problemsview/BlazeProblemsViewConsole.java
@@ -36,7 +36,7 @@
 import com.intellij.ui.content.Content;
 import com.intellij.ui.content.ContentFactory;
 import com.intellij.util.ArrayUtil;
-import com.intellij.util.concurrency.SequentialTaskExecutor;
+import com.intellij.util.concurrency.BoundedTaskExecutor;
 import com.intellij.util.ui.MessageCategory;
 import com.intellij.util.ui.UIUtil;
 import java.util.ArrayList;
@@ -44,6 +44,7 @@
 import java.util.List;
 import java.util.StringTokenizer;
 import java.util.UUID;
+import java.util.concurrent.ExecutorService;
 import javax.swing.Icon;
 import org.jetbrains.annotations.NotNull;
 import org.jetbrains.annotations.Nullable;
@@ -58,8 +59,8 @@
       EnumSet.allOf(ErrorTreeElementKind.class);
 
   private final ProblemsViewPanel myPanel;
-  private final SequentialTaskExecutor myViewUpdater =
-      new SequentialTaskExecutor(PooledThreadExecutor.INSTANCE);
+  private final ExecutorService myViewUpdater =
+      new BoundedTaskExecutor(PooledThreadExecutor.INSTANCE, 1);
   private final Icon myActiveIcon = AllIcons.Toolwindows.Problems;
   private final Icon myPassiveIcon = IconLoader.getDisabledIcon(myActiveIcon);
 
diff --git a/clwb/src/com/google/idea/blaze/clwb/run/BlazeCidrDebuggableConfigurationFactory.java b/clwb/src/com/google/idea/blaze/clwb/run/BlazeCidrDebuggableConfigurationFactory.java
index 5363e69..55b0592 100644
--- a/clwb/src/com/google/idea/blaze/clwb/run/BlazeCidrDebuggableConfigurationFactory.java
+++ b/clwb/src/com/google/idea/blaze/clwb/run/BlazeCidrDebuggableConfigurationFactory.java
@@ -15,9 +15,7 @@
  */
 package com.google.idea.blaze.clwb.run;
 
-import com.google.common.collect.ImmutableList;
 import com.google.idea.blaze.base.command.BlazeCommandName;
-import com.google.idea.blaze.base.command.BlazeFlags;
 import com.google.idea.blaze.base.ideinfo.TargetIdeInfo;
 import com.google.idea.blaze.base.ideinfo.TargetKey;
 import com.google.idea.blaze.base.model.BlazeProjectData;
@@ -57,10 +55,9 @@
     Kind kind = blazeConfig.getKindForTarget();
     if (state != null) {
       if (kind != null && Kind.isTestRule(kind.toString())) {
-        state.setCommand(BlazeCommandName.TEST);
-        state.setBlazeFlags(ImmutableList.of(BlazeFlags.TEST_OUTPUT_STREAMED));
+        state.getCommandState().setCommand(BlazeCommandName.TEST);
       } else {
-        state.setCommand(BlazeCommandName.RUN);
+        state.getCommandState().setCommand(BlazeCommandName.RUN);
       }
     }
     blazeConfig.setGeneratedName();
diff --git a/clwb/src/com/google/idea/blaze/clwb/run/BlazeCidrLauncher.java b/clwb/src/com/google/idea/blaze/clwb/run/BlazeCidrLauncher.java
index 410cf57..473400b 100644
--- a/clwb/src/com/google/idea/blaze/clwb/run/BlazeCidrLauncher.java
+++ b/clwb/src/com/google/idea/blaze/clwb/run/BlazeCidrLauncher.java
@@ -26,14 +26,18 @@
 import com.google.idea.blaze.base.projectview.ProjectViewManager;
 import com.google.idea.blaze.base.projectview.ProjectViewSet;
 import com.google.idea.blaze.base.run.BlazeCommandRunConfiguration;
+import com.google.idea.blaze.base.run.DistributedExecutorSupport;
 import com.google.idea.blaze.base.run.processhandler.LineProcessingProcessAdapter;
 import com.google.idea.blaze.base.run.processhandler.ScopedBlazeProcessHandler;
+import com.google.idea.blaze.base.run.smrunner.BlazeTestEventsHandler;
+import com.google.idea.blaze.base.run.smrunner.SmRunnerUtils;
 import com.google.idea.blaze.base.run.state.BlazeCommandRunConfigurationCommonState;
 import com.google.idea.blaze.base.scope.BlazeContext;
 import com.google.idea.blaze.base.scope.scopes.IssuesScope;
 import com.google.idea.blaze.base.settings.Blaze;
 import com.google.idea.blaze.base.settings.BlazeImportSettings;
 import com.google.idea.blaze.base.settings.BlazeImportSettingsManager;
+import com.google.idea.sdkcompat.cidr.CidrConsoleBuilderAdapter;
 import com.intellij.execution.ExecutionException;
 import com.intellij.execution.configurations.CommandLineState;
 import com.intellij.execution.configurations.GeneralCommandLine;
@@ -50,10 +54,8 @@
 import com.jetbrains.cidr.execution.debugger.CidrDebugProcess;
 import com.jetbrains.cidr.execution.debugger.CidrLocalDebugProcess;
 import com.jetbrains.cidr.execution.testing.CidrLauncher;
-import com.jetbrains.cidr.execution.testing.OCGoogleTestConsoleProperties;
 import java.io.File;
-import java.util.Objects;
-import org.jetbrains.annotations.NotNull;
+import javax.annotation.Nullable;
 
 /**
  * Handles running/debugging cc_test and cc_binary targets in CLion. Sets up gdb when debugging, and
@@ -68,7 +70,7 @@
   private final BlazeCidrRunConfigurationRunner runner;
   private final ExecutionEnvironment executionEnvironment;
 
-  public BlazeCidrLauncher(
+  BlazeCidrLauncher(
       BlazeCommandRunConfiguration configuration,
       BlazeCidrRunConfigurationRunner runner,
       ExecutionEnvironment environment) {
@@ -89,26 +91,40 @@
     ProjectViewSet projectViewSet = ProjectViewManager.getInstance(project).getProjectViewSet();
     LOG.assertTrue(projectViewSet != null);
 
-    state.setConsoleBuilder(createConsoleBuilder());
+    ImmutableList<String> testHandlerFlags = ImmutableList.of();
+    BlazeTestEventsHandler testEventsHandler =
+        useTestUi()
+            ? BlazeTestEventsHandler.getHandlerForTarget(project, configuration.getTarget())
+            : null;
+    if (testEventsHandler != null) {
+      testHandlerFlags = BlazeTestEventsHandler.getBlazeFlags(project);
+    }
 
-    BlazeCommand blazeCommand =
-        BlazeCommand.builder(Blaze.getBuildSystem(project), handlerState.getCommand())
+    BlazeCommand.Builder command =
+        BlazeCommand.builder(
+                Blaze.getBuildSystemProvider(project).getBinaryPath(),
+                handlerState.getCommandState().getCommand())
             .addTargets(configuration.getTarget())
             .addBlazeFlags(BlazeFlags.buildFlags(project, ProjectViewSet.builder().build()))
-            .addBlazeFlags(handlerState.getBlazeFlags())
-            .addExeFlags(handlerState.getExeFlags())
-            .build();
+            .addBlazeFlags(testHandlerFlags)
+            .addBlazeFlags(handlerState.getBlazeFlagsState().getExpandedFlags())
+            .addExeFlags(handlerState.getExeFlagsState().getExpandedFlags());
+
+    command.addBlazeFlags(
+        DistributedExecutorSupport.getBlazeFlags(
+            project, handlerState.getRunOnDistributedExecutorState().runOnDistributedExecutor));
+
+    state.setConsoleBuilder(createConsoleBuilder(testEventsHandler));
 
     WorkspaceRoot workspaceRoot = WorkspaceRoot.fromImportSettings(importSettings);
     return new ScopedBlazeProcessHandler(
         project,
-        blazeCommand,
+        command.build(),
         workspaceRoot,
         new ScopedBlazeProcessHandler.ScopedProcessHandlerDelegate() {
           @Override
           public void onBlazeContextStart(BlazeContext context) {
-            context
-                .push(new IssuesScope(project));
+            context.push(new IssuesScope(project));
           }
 
           @Override
@@ -138,31 +154,28 @@
     GeneralCommandLine commandLine = new GeneralCommandLine(runner.executableToDebug.getPath());
     File workingDir = workspaceRoot.directory();
     commandLine.setWorkDirectory(workingDir);
-    commandLine.addParameters(handlerState.getExeFlags());
+    commandLine.addParameters(handlerState.getExeFlagsState().getExpandedFlags());
 
     TrivialInstaller installer = new TrivialInstaller(commandLine);
     ImmutableList<String> startupCommands = getGdbStartupCommands(workingDir);
     CLionRunParameters parameters =
         new CLionRunParameters(
             new BlazeGDBDriverConfiguration(project, startupCommands, workspaceRoot), installer);
-    CidrDebugProcess result =
-        new CidrLocalDebugProcess(parameters, session, state.getConsoleBuilder());
 
-    return result;
+    return new CidrLocalDebugProcess(parameters, session, state.getConsoleBuilder());
   }
 
-  @NotNull
   @Override
   protected Project getProject() {
     return project;
   }
 
-  private CidrConsoleBuilder createConsoleBuilder() {
-    if (Objects.equals(handlerState.getCommand(), BlazeCommandName.TEST)) {
-      // Use the Google Test failure/success console instead of a standard console.
-      return new GoogleTestConsoleBuilder(configuration.getProject());
+  private CidrConsoleBuilder createConsoleBuilder(
+      @Nullable BlazeTestEventsHandler testEventsHandler) {
+    if (testEventsHandler != null) {
+      return new GoogleTestConsoleBuilder(configuration.getProject(), testEventsHandler);
     }
-    return new CidrConsoleBuilder(configuration.getProject());
+    return new CidrConsoleBuilderAdapter(configuration.getProject());
   }
 
   private ImmutableList<String> getGdbStartupCommands(File workingDir) {
@@ -176,20 +189,27 @@
     return ImmutableList.of(subPathCommand);
   }
 
-  private final class GoogleTestConsoleBuilder extends CidrConsoleBuilder {
-    private GoogleTestConsoleBuilder(Project project) {
+  private boolean useTestUi() {
+    return BlazeCommandName.TEST.equals(handlerState.getCommandState().getCommand())
+        && !handlerState.getRunOnDistributedExecutorState().runOnDistributedExecutor;
+  }
+
+  private final class GoogleTestConsoleBuilder extends CidrConsoleBuilderAdapter {
+    private final BlazeTestEventsHandler testEventsHandler;
+
+    private GoogleTestConsoleBuilder(Project project, BlazeTestEventsHandler testEventsHandler) {
       super(project);
+      this.testEventsHandler = testEventsHandler;
       addFilter(new BlazeCidrTestOutputFilter(project));
     }
 
     @Override
     protected ConsoleView createConsole() {
-      OCGoogleTestConsoleProperties consoleProperties =
-          new OCGoogleTestConsoleProperties(
-              configuration,
-              executionEnvironment.getExecutor(),
-              executionEnvironment.getExecutionTarget());
-      return createConsole(configuration.getType(), consoleProperties);
+      return SmRunnerUtils.getConsoleView(
+          configuration.getProject(),
+          configuration,
+          executionEnvironment.getExecutor(),
+          testEventsHandler);
     }
   }
 }
diff --git a/clwb/src/com/google/idea/blaze/clwb/run/BlazeCidrRunConfigurationHandler.java b/clwb/src/com/google/idea/blaze/clwb/run/BlazeCidrRunConfigurationHandler.java
index c5ded30..6a6f411 100644
--- a/clwb/src/com/google/idea/blaze/clwb/run/BlazeCidrRunConfigurationHandler.java
+++ b/clwb/src/com/google/idea/blaze/clwb/run/BlazeCidrRunConfigurationHandler.java
@@ -78,7 +78,7 @@
   @Override
   @Nullable
   public String getCommandName() {
-    BlazeCommandName command = state.getCommand();
+    BlazeCommandName command = state.getCommandState().getCommand();
     return command != null ? command.toString() : null;
   }
 
diff --git a/clwb/src/com/google/idea/blaze/clwb/run/BlazeCidrRunConfigurationRunner.java b/clwb/src/com/google/idea/blaze/clwb/run/BlazeCidrRunConfigurationRunner.java
index 208cf11..42a78c7 100644
--- a/clwb/src/com/google/idea/blaze/clwb/run/BlazeCidrRunConfigurationRunner.java
+++ b/clwb/src/com/google/idea/blaze/clwb/run/BlazeCidrRunConfigurationRunner.java
@@ -15,16 +15,15 @@
  */
 package com.google.idea.blaze.clwb.run;
 
+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.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.buildresult.BuildResultHelper;
 import com.google.idea.blaze.base.issueparser.IssueOutputLineProcessor;
 import com.google.idea.blaze.base.model.primitives.WorkspaceRoot;
 import com.google.idea.blaze.base.projectview.ProjectViewManager;
@@ -50,7 +49,6 @@
 import com.intellij.openapi.vfs.LocalFileSystem;
 import com.jetbrains.cidr.execution.CidrCommandLineState;
 import java.io.File;
-import java.util.List;
 
 /** CLion-specific handler for {@link BlazeCommandRunConfiguration}s. */
 public class BlazeCidrRunConfigurationRunner implements BlazeCommandRunConfigurationRunner {
@@ -109,7 +107,8 @@
     final ProjectViewSet projectViewSet =
         ProjectViewManager.getInstance(project).getProjectViewSet();
 
-    final List<File> outputArtifacts = Lists.newArrayList();
+    BuildResultHelper buildResultHelper = BuildResultHelper.forFiles(file -> true);
+
     final ListenableFuture<Void> buildOperation =
         BlazeExecutor.submitTask(
             project,
@@ -123,12 +122,13 @@
                 context.output(new StatusOutput("Building debug binary"));
 
                 BlazeCommand.Builder command =
-                    BlazeCommand.builder(Blaze.getBuildSystem(project), BlazeCommandName.BUILD)
+                    BlazeCommand.builder(
+                            Blaze.getBuildSystemProvider(project).getBinaryPath(),
+                            BlazeCommandName.BUILD)
                         .addTargets(configuration.getTarget())
                         .addBlazeFlags(BlazeFlags.buildFlags(project, projectViewSet))
-                        .addBlazeFlags(handlerState.getBlazeFlags());
-
-                command.addBlazeFlags("--experimental_show_artifacts");
+                        .addBlazeFlags(handlerState.getBlazeFlagsState().getExpandedFlags())
+                        .addBlazeFlags(buildResultHelper.getBuildFlags());
 
                 // If we are trying to debug, make sure we are building in debug mode.
                 // This can cause a rebuild, so it is a heavyweight setting.
@@ -140,8 +140,7 @@
                     .addBlazeCommand(command.build())
                     .context(context)
                     .stderr(
-                        LineProcessingOutputStream.of(
-                            new ExperimentalShowArtifactsLineProcessor(outputArtifacts),
+                        buildResultHelper.stderr(
                             new IssueOutputLineProcessor(project, context, workspaceRoot)))
                     .build()
                     .run();
@@ -154,6 +153,7 @@
     } catch (InterruptedException | java.util.concurrent.ExecutionException e) {
       throw new ExecutionException(e);
     }
+    ImmutableList<File> outputArtifacts = buildResultHelper.getBuildArtifacts();
     if (outputArtifacts.isEmpty()) {
       throw new ExecutionException(
           String.format("No output artifacts found when building %s", configuration.getTarget()));
diff --git a/clwb/src/com/google/idea/blaze/clwb/run/BlazeCidrTestOutputFilter.java b/clwb/src/com/google/idea/blaze/clwb/run/BlazeCidrTestOutputFilter.java
index b2498f1..cdd8900 100644
--- a/clwb/src/com/google/idea/blaze/clwb/run/BlazeCidrTestOutputFilter.java
+++ b/clwb/src/com/google/idea/blaze/clwb/run/BlazeCidrTestOutputFilter.java
@@ -22,7 +22,7 @@
 import com.intellij.execution.filters.RegexpFilter;
 import com.intellij.openapi.project.Project;
 import java.io.File;
-import org.jetbrains.annotations.Nullable;
+import javax.annotation.Nullable;
 
 /** Identifies file paths in CLion test output which aren't found by CidrPathConsoleFilter. */
 public class BlazeCidrTestOutputFilter extends RegexpFilter {
diff --git a/clwb/src/com/google/idea/blaze/clwb/run/BlazeCommandFlags.java b/clwb/src/com/google/idea/blaze/clwb/run/BlazeCommandFlags.java
deleted file mode 100644
index 12020f2..0000000
--- a/clwb/src/com/google/idea/blaze/clwb/run/BlazeCommandFlags.java
+++ /dev/null
@@ -1,132 +0,0 @@
-/*
- * Copyright 2016 The Bazel Authors. All rights reserved.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.google.idea.blaze.clwb.run;
-
-import com.google.common.base.Objects;
-import com.google.common.base.Strings;
-import com.google.common.collect.ImmutableList;
-import com.google.idea.blaze.base.ui.UiUtil;
-import com.intellij.openapi.util.InvalidDataException;
-import com.intellij.openapi.util.JDOMExternalizable;
-import com.intellij.openapi.util.WriteExternalException;
-import com.intellij.util.execution.ParametersListUtil;
-import java.util.List;
-import javax.swing.JComponent;
-import javax.swing.JLabel;
-import javax.swing.JTextArea;
-import org.jdom.Element;
-
-final class BlazeCommandFlags implements JDOMExternalizable {
-  public static final class Editor {
-    private final JTextArea blazeFlagsField = new JTextArea(5, 0);
-    private final JTextArea exeFlagsField = new JTextArea(5, 0);
-
-    public JComponent getEditorComponent() {
-      return UiUtil.createBox(
-          new JLabel("Blaze flags:"),
-          blazeFlagsField,
-          new JLabel("Executable flags:"),
-          exeFlagsField);
-    }
-
-    public void setText(BlazeCommandFlags blazeCommandFlags) {
-      blazeFlagsField.setText(ParametersListUtil.join(blazeCommandFlags.getBlazeFlags()));
-      exeFlagsField.setText(ParametersListUtil.join(blazeCommandFlags.getExeFlags()));
-    }
-
-    public BlazeCommandFlags getBlazeCommandFlags() {
-      ImmutableList<String> blazeFlags =
-          ImmutableList.copyOf(
-              ParametersListUtil.parse(Strings.nullToEmpty(blazeFlagsField.getText())));
-      ImmutableList<String> exeFlags =
-          ImmutableList.copyOf(
-              ParametersListUtil.parse(Strings.nullToEmpty(exeFlagsField.getText())));
-      return new BlazeCommandFlags(blazeFlags, exeFlags);
-    }
-  }
-
-  private static final String USER_BLAZE_FLAG_TAG = "blaze-user-flag";
-  private static final String USER_EXE_FLAG_TAG = "blaze-user-exe-flag";
-
-  private ImmutableList<String> blazeFlags = ImmutableList.of();
-  private ImmutableList<String> exeFlags = ImmutableList.of();
-
-  public BlazeCommandFlags() {
-    this.blazeFlags = ImmutableList.of();
-    this.exeFlags = ImmutableList.of();
-  }
-
-  public BlazeCommandFlags(ImmutableList<String> blazeFlags, ImmutableList<String> exeFlags) {
-    this.blazeFlags = blazeFlags;
-    this.exeFlags = exeFlags;
-  }
-
-  public ImmutableList<String> getBlazeFlags() {
-    return blazeFlags;
-  }
-
-  public ImmutableList<String> getExeFlags() {
-    return exeFlags;
-  }
-
-  @Override
-  public void readExternal(Element element) throws InvalidDataException {
-    blazeFlags = loadUserFlags(element, USER_BLAZE_FLAG_TAG);
-    exeFlags = loadUserFlags(element, USER_EXE_FLAG_TAG);
-  }
-
-  private static ImmutableList<String> loadUserFlags(Element root, String tag) {
-    ImmutableList.Builder<String> flagsBuilder = ImmutableList.builder();
-    for (Element e : root.getChildren(tag)) {
-      String flag = e.getTextTrim();
-      if (flag != null && !flag.isEmpty()) {
-        flagsBuilder.add(flag);
-      }
-    }
-    return flagsBuilder.build();
-  }
-
-  @Override
-  public void writeExternal(Element element) throws WriteExternalException {
-    saveUserFlags(element, blazeFlags, USER_BLAZE_FLAG_TAG);
-    saveUserFlags(element, exeFlags, USER_EXE_FLAG_TAG);
-  }
-
-  private static void saveUserFlags(Element root, List<String> flags, String tag) {
-    for (String flag : flags) {
-      Element child = new Element(tag);
-      child.setText(flag);
-      root.addContent(child);
-    }
-  }
-
-  @Override
-  public boolean equals(Object o) {
-    if (this == o) {
-      return true;
-    }
-    if (o == null || getClass() != o.getClass()) {
-      return false;
-    }
-    BlazeCommandFlags that = (BlazeCommandFlags) o;
-    return Objects.equal(blazeFlags, that.blazeFlags) && Objects.equal(exeFlags, that.exeFlags);
-  }
-
-  @Override
-  public int hashCode() {
-    return Objects.hashCode(blazeFlags, exeFlags);
-  }
-}
diff --git a/clwb/src/com/google/idea/blaze/clwb/run/RunConfigurationUtils.java b/clwb/src/com/google/idea/blaze/clwb/run/RunConfigurationUtils.java
index 10e7c7f..21845ca 100644
--- a/clwb/src/com/google/idea/blaze/clwb/run/RunConfigurationUtils.java
+++ b/clwb/src/com/google/idea/blaze/clwb/run/RunConfigurationUtils.java
@@ -34,7 +34,7 @@
     if (handlerState == null) {
       return false;
     }
-    BlazeCommandName command = handlerState.getCommand();
+    BlazeCommandName command = handlerState.getCommandState().getCommand();
     return kind != null
         && command != null
         && ((kind == Kind.CC_TEST && command.equals(BlazeCommandName.TEST))
diff --git a/clwb/src/com/google/idea/blaze/clwb/run/producers/BlazeCidrTestConfigurationProducer.java b/clwb/src/com/google/idea/blaze/clwb/run/producers/BlazeCidrTestConfigurationProducer.java
index 3589c9a..cf7f91f 100644
--- a/clwb/src/com/google/idea/blaze/clwb/run/producers/BlazeCidrTestConfigurationProducer.java
+++ b/clwb/src/com/google/idea/blaze/clwb/run/producers/BlazeCidrTestConfigurationProducer.java
@@ -17,7 +17,6 @@
 
 import com.google.common.collect.ImmutableList;
 import com.google.idea.blaze.base.command.BlazeCommandName;
-import com.google.idea.blaze.base.command.BlazeFlags;
 import com.google.idea.blaze.base.model.primitives.Label;
 import com.google.idea.blaze.base.run.BlazeCommandRunConfiguration;
 import com.google.idea.blaze.base.run.BlazeCommandRunConfigurationType;
@@ -25,25 +24,13 @@
 import com.google.idea.blaze.base.run.producers.BlazeRunConfigurationProducer;
 import com.google.idea.blaze.base.run.state.BlazeCommandRunConfigurationCommonState;
 import com.google.idea.blaze.base.settings.Blaze;
+import com.google.idea.blaze.clwb.run.test.GoogleTestLocation;
+import com.google.idea.blaze.clwb.run.test.GoogleTestSpecification;
 import com.intellij.execution.Location;
 import com.intellij.execution.actions.ConfigurationContext;
 import com.intellij.openapi.actionSystem.LangDataKeys;
-import com.intellij.openapi.util.Couple;
 import com.intellij.openapi.util.Ref;
 import com.intellij.psi.PsiElement;
-import com.intellij.psi.util.PsiTreeUtil;
-import com.jetbrains.cidr.execution.testing.CidrTestUtil;
-import com.jetbrains.cidr.lang.psi.OCFile;
-import com.jetbrains.cidr.lang.psi.OCFunctionDefinition;
-import com.jetbrains.cidr.lang.psi.OCMacroCall;
-import com.jetbrains.cidr.lang.psi.OCMacroCallArgument;
-import com.jetbrains.cidr.lang.psi.OCStruct;
-import com.jetbrains.cidr.lang.symbols.OCSymbol;
-import com.jetbrains.cidr.lang.symbols.cpp.OCFunctionSymbol;
-import com.jetbrains.cidr.lang.symbols.cpp.OCStructSymbol;
-import com.jetbrains.cidr.lang.symbols.cpp.OCSymbolWithQualifiedName;
-import java.util.Collection;
-import java.util.List;
 import java.util.Objects;
 import javax.annotation.Nullable;
 
@@ -51,52 +38,6 @@
 public class BlazeCidrTestConfigurationProducer
     extends BlazeRunConfigurationProducer<BlazeCommandRunConfiguration> {
 
-  private static class TestTarget {
-    @Nullable
-    private static TestTarget createFromFile(@Nullable PsiElement element) {
-      return createFromClassAndMethod(element, null, null);
-    }
-
-    @Nullable
-    private static TestTarget createFromClass(@Nullable PsiElement element, String className) {
-      return createFromClassAndMethod(element, className, null);
-    }
-
-    @Nullable
-    private static TestTarget createFromClassAndMethod(
-        @Nullable PsiElement element, String classOrSuiteName, @Nullable String testName) {
-      Label label = TestTargetHeuristic.testTargetForPsiElement(element);
-      if (label == null) {
-        return null;
-      }
-      String filter = null;
-      if (classOrSuiteName != null) {
-        filter = classOrSuiteName;
-        if (testName != null) {
-          filter += "." + testName;
-        }
-      }
-      return new TestTarget(element, label, filter);
-    }
-
-    private final PsiElement element;
-    private final Label label;
-    @Nullable private final String testFilterArg;
-    private final String name;
-
-    private TestTarget(PsiElement element, Label label, @Nullable String testFilter) {
-      this.element = element;
-      this.label = label;
-      if (testFilter != null) {
-        testFilterArg = BlazeFlags.TEST_FILTER + "=" + testFilter;
-        name = String.format("%s (%s)", testFilter, label.toString());
-      } else {
-        testFilterArg = null;
-        name = label.toString();
-      }
-    }
-  }
-
   public BlazeCidrTestConfigurationProducer() {
     super(BlazeCommandRunConfigurationType.getInstance());
   }
@@ -122,30 +63,35 @@
     if (element == null) {
       return false;
     }
-    TestTarget testObject = findTestObject(element);
-    if (testObject == null) {
+    GoogleTestLocation test = GoogleTestLocation.findGoogleTest(element);
+    if (test == null) {
       return false;
     }
-    sourceElement.set(testObject.element);
-    configuration.setTarget(testObject.label);
+    Label label = getTestTarget(test.getPsiElement());
+    if (label == null) {
+      return false;
+    }
+    sourceElement.set(test.getPsiElement());
+    configuration.setTarget(label);
     BlazeCommandRunConfigurationCommonState handlerState =
         configuration.getHandlerStateIfType(BlazeCommandRunConfigurationCommonState.class);
     if (handlerState == null) {
       return false;
     }
-    handlerState.setCommand(BlazeCommandName.TEST);
+    handlerState.getCommandState().setCommand(BlazeCommandName.TEST);
 
     ImmutableList.Builder<String> flags = ImmutableList.builder();
-    if (testObject.testFilterArg != null) {
-      flags.add(testObject.testFilterArg);
+    String testFilter = test.getTestFilterFlag();
+    if (testFilter != null) {
+      flags.add(testFilter);
     }
-    flags.add(BlazeFlags.TEST_OUTPUT_STREAMED);
-    flags.addAll(handlerState.getBlazeFlags());
+    flags.addAll(handlerState.getBlazeFlagsState().getRawFlags());
 
-    handlerState.setBlazeFlags(flags.build());
+    handlerState.getBlazeFlagsState().setRawFlags(flags.build());
     configuration.setName(
         String.format(
-            "%s test: %s", Blaze.buildSystemName(configuration.getProject()), testObject.name));
+            "%s test: %s",
+            Blaze.buildSystemName(configuration.getProject()), getTestName(label, test.gtest)));
     return true;
   }
 
@@ -157,107 +103,34 @@
     if (handlerState == null) {
       return false;
     }
-    if (!Objects.equals(handlerState.getCommand(), BlazeCommandName.TEST)) {
+    if (!Objects.equals(handlerState.getCommandState().getCommand(), BlazeCommandName.TEST)) {
       return false;
     }
     PsiElement element = selectedPsiElement(context);
     if (element == null) {
       return false;
     }
-    TestTarget testObject = findTestObject(element);
-    if (testObject == null) {
+    GoogleTestLocation test = GoogleTestLocation.findGoogleTest(element);
+    if (test == null) {
       return false;
     }
-    List<String> flags = handlerState.getBlazeFlags();
-    return testObject.label.equals(configuration.getTarget())
-        && (testObject.testFilterArg == null || flags.contains(testObject.testFilterArg));
+    Label label = getTestTarget(test.getPsiElement());
+    if (label == null) {
+      return false;
+    }
+    return label.equals(configuration.getTarget())
+        && Objects.equals(handlerState.getTestFilterFlag(), test.getTestFilterFlag());
   }
 
   @Nullable
-  private static TestTarget findTestObject(PsiElement element) {
-    // Copied from on CidrGoogleTestRunConfigurationProducer::findTestObject.
-    // Precedence order (decreasing): class/function, macro, file
-    PsiElement parent =
-        PsiTreeUtil.getNonStrictParentOfType(element, OCFunctionDefinition.class, OCStruct.class);
-
-    OCStructSymbol parentSymbol;
-    if (parent instanceof OCStruct
-        && ((parentSymbol = ((OCStruct) parent).getSymbol()) != null)
-        && CidrTestUtil.isGoogleTestClass(parentSymbol)) {
-      Couple<String> name = CidrTestUtil.extractGoogleTestName(parentSymbol);
-      if (name != null) {
-        return TestTarget.createFromClassAndMethod(parent, name.first, name.second);
-      }
-      String className = parentSymbol.getQualifiedName().getName();
-      return TestTarget.createFromClass(parent, className);
-    } else if (parent instanceof OCFunctionDefinition) {
-      OCFunctionSymbol symbol = ((OCFunctionDefinition) parent).getSymbol();
-      if (symbol != null) {
-        OCSymbolWithQualifiedName<?> resolvedOwner = symbol.getResolvedOwner();
-        if (resolvedOwner != null) {
-          OCSymbol<?> owner = resolvedOwner.getDefinitionSymbol();
-          if (owner instanceof OCStructSymbol
-              && CidrTestUtil.isGoogleTestClass((OCStructSymbol) owner)) {
-            OCStruct struct = (OCStruct) owner.locateDefinition();
-            Couple<String> name = CidrTestUtil.extractGoogleTestName((OCStructSymbol) owner);
-            if (name != null) {
-              return TestTarget.createFromClassAndMethod(struct, name.first, name.second);
-            }
-            return TestTarget.createFromClass(
-                struct, ((OCStructSymbol) owner).getQualifiedName().getName());
-          }
-        }
-      }
-    }
-
-    // if we're still here, let's test for a macro and, as a last resort, a file.
-    parent = PsiTreeUtil.getNonStrictParentOfType(element, OCMacroCall.class, OCFile.class);
-    if (parent instanceof OCMacroCall) {
-      OCMacroCall gtestMacro = CidrTestUtil.findGoogleTestMacros(parent);
-      if (gtestMacro != null) {
-        List<OCMacroCallArgument> arguments = gtestMacro.getArguments();
-        if (arguments.size() >= 2) {
-          OCMacroCallArgument suiteArg = arguments.get(0);
-          OCMacroCallArgument testArg = arguments.get(1);
-
-          // if the element is the first argument of macro call,
-          // then running entire suite, otherwise only a current test
-          boolean isSuite =
-              isFirstArgument(PsiTreeUtil.getParentOfType(element, OCMacroCallArgument.class))
-                  || isFirstArgument(element.getPrevSibling());
-          String suiteName = CidrTestUtil.extractArgumentValue(suiteArg);
-          String testName = CidrTestUtil.extractArgumentValue(testArg);
-          OCStructSymbol symbol =
-              CidrTestUtil.findGoogleTestSymbol(element.getProject(), suiteName, testName);
-          if (symbol != null) {
-            OCStruct targetElement = (OCStruct) symbol.locateDefinition();
-            return TestTarget.createFromClassAndMethod(
-                targetElement, suiteName, isSuite ? null : testName);
-          }
-        }
-      }
-      Couple<String> suite = CidrTestUtil.extractFullSuiteNameFromMacro(parent);
-      if (suite != null) {
-        Collection<OCStructSymbol> res =
-            CidrTestUtil.findGoogleTestSymbolsForSuiteRandomly(
-                element.getProject(), suite.first, true);
-        if (res.size() != 0) {
-          OCStruct struct = (OCStruct) res.iterator().next().locateDefinition();
-          return TestTarget.createFromClassAndMethod(struct, suite.first, null);
-        }
-      }
-    } else if (parent instanceof OCFile) {
-      return TestTarget.createFromFile(parent);
-    }
-    return null;
+  private static Label getTestTarget(PsiElement element) {
+    return TestTargetHeuristic.testTargetForPsiElement(element);
   }
 
-  private static boolean isFirstArgument(@Nullable PsiElement element) {
-    OCMacroCall macroCall = PsiTreeUtil.getParentOfType(element, OCMacroCall.class);
-    if (macroCall != null) {
-      List<OCMacroCallArgument> arguments = macroCall.getArguments();
-      return arguments.size() > 0 && arguments.get(0).equals(element);
-    }
-    return false;
+  private static String getTestName(Label target, GoogleTestSpecification gtest) {
+    String filterDescription = gtest.description();
+    return filterDescription != null
+        ? String.format("%s (%s)", filterDescription, target.toString())
+        : target.toString();
   }
 }
diff --git a/clwb/src/com/google/idea/blaze/clwb/run/producers/NonBlazeProducerSuppressor.java b/clwb/src/com/google/idea/blaze/clwb/run/producers/NonBlazeProducerSuppressor.java
index 8f163a7..189ba34 100644
--- a/clwb/src/com/google/idea/blaze/clwb/run/producers/NonBlazeProducerSuppressor.java
+++ b/clwb/src/com/google/idea/blaze/clwb/run/producers/NonBlazeProducerSuppressor.java
@@ -15,20 +15,15 @@
  */
 package com.google.idea.blaze.clwb.run.producers;
 
-import com.google.common.collect.ImmutableList;
 import com.google.idea.blaze.base.settings.Blaze;
+import com.google.idea.sdkcompat.clion.CMakeConfigurationProducersList;
 import com.intellij.execution.RunConfigurationProducerService;
-import com.intellij.execution.actions.RunConfigurationProducer;
 import com.intellij.openapi.components.AbstractProjectComponent;
 import com.intellij.openapi.project.Project;
-import com.jetbrains.cidr.cpp.execution.testing.CMakeGoogleTestRunConfigurationProducer;
-import java.util.Collection;
 
 /** Suppresses certain non-Blaze configuration producers in Blaze projects. */
 public class NonBlazeProducerSuppressor extends AbstractProjectComponent {
 
-  private static final Collection<Class<? extends RunConfigurationProducer<?>>>
-      PRODUCERS_TO_SUPPRESS = ImmutableList.of(CMakeGoogleTestRunConfigurationProducer.class);
 
   public NonBlazeProducerSuppressor(Project project) {
     super(project);
@@ -44,6 +39,7 @@
   private static void suppressProducers(Project project) {
     RunConfigurationProducerService producerService =
         RunConfigurationProducerService.getInstance(project);
-    PRODUCERS_TO_SUPPRESS.forEach(producerService::addIgnoredProducer);
+    CMakeConfigurationProducersList.PRODUCERS_TO_SUPPRESS.forEach(
+        producerService::addIgnoredProducer);
   }
 }
diff --git a/clwb/src/com/google/idea/blaze/clwb/run/test/BlazeCidrTestEventsHandler.java b/clwb/src/com/google/idea/blaze/clwb/run/test/BlazeCidrTestEventsHandler.java
new file mode 100644
index 0000000..f117615
--- /dev/null
+++ b/clwb/src/com/google/idea/blaze/clwb/run/test/BlazeCidrTestEventsHandler.java
@@ -0,0 +1,58 @@
+/*
+ * Copyright 2017 The Bazel Authors. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.idea.blaze.clwb.run.test;
+
+import com.google.common.base.Joiner;
+import com.google.idea.blaze.base.command.BlazeFlags;
+import com.google.idea.blaze.base.model.primitives.Kind;
+import com.google.idea.blaze.base.run.smrunner.BlazeTestEventsHandler;
+import com.intellij.execution.Location;
+import com.intellij.execution.testframework.sm.runner.SMTestLocator;
+import com.intellij.openapi.project.Project;
+import java.util.ArrayList;
+import java.util.EnumSet;
+import java.util.List;
+import javax.annotation.Nullable;
+
+/** Provides C/C++ specific methods needed by the SM-runner test UI. */
+public class BlazeCidrTestEventsHandler extends BlazeTestEventsHandler {
+
+  @Override
+  protected EnumSet<Kind> handledKinds() {
+    return EnumSet.of(Kind.CC_TEST);
+  }
+
+  @Override
+  public SMTestLocator getTestLocator() {
+    return BlazeCppTestLocator.INSTANCE;
+  }
+
+  @Nullable
+  @Override
+  public String getTestFilter(Project project, List<Location<?>> testLocations) {
+    List<String> filters = new ArrayList<>();
+    for (Location<?> location : testLocations) {
+      GoogleTestLocation test = GoogleTestLocation.findGoogleTest(location);
+      if (test != null && test.testFilter != null) {
+        filters.add(test.testFilter);
+      }
+    }
+    if (filters.isEmpty()) {
+      return null;
+    }
+    return String.format("%s=%s", BlazeFlags.TEST_FILTER, Joiner.on(':').join(filters));
+  }
+}
diff --git a/clwb/src/com/google/idea/blaze/clwb/run/test/BlazeCppTestLocator.java b/clwb/src/com/google/idea/blaze/clwb/run/test/BlazeCppTestLocator.java
new file mode 100644
index 0000000..cd02fc0
--- /dev/null
+++ b/clwb/src/com/google/idea/blaze/clwb/run/test/BlazeCppTestLocator.java
@@ -0,0 +1,136 @@
+/*
+ * Copyright 2017 The Bazel Authors. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.idea.blaze.clwb.run.test;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Iterables;
+import com.google.idea.blaze.base.run.smrunner.SmRunnerUtils;
+import com.google.idea.sdkcompat.cidr.CidrGoogleTestUtilAdapter;
+import com.intellij.execution.Location;
+import com.intellij.execution.testframework.sm.runner.SMTestLocator;
+import com.intellij.openapi.project.Project;
+import com.intellij.psi.PsiElement;
+import com.intellij.psi.search.GlobalSearchScope;
+import com.intellij.util.CommonProcessors;
+import com.jetbrains.cidr.lang.psi.OCMacroCall;
+import com.jetbrains.cidr.lang.psi.OCStruct;
+import com.jetbrains.cidr.lang.symbols.OCSymbol;
+import com.jetbrains.cidr.lang.symbols.cpp.OCStructSymbol;
+import com.jetbrains.cidr.lang.symbols.symtable.OCGlobalProjectSymbolsCache;
+import java.util.Collection;
+import java.util.List;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+import javax.annotation.Nullable;
+
+/** Locate cpp test classes / methods for test UI navigation. */
+public class BlazeCppTestLocator implements SMTestLocator {
+
+  public static final BlazeCppTestLocator INSTANCE = new BlazeCppTestLocator();
+
+  private static final Pattern SUITE_PATTERN = Pattern.compile("((\\w+)/)?(\\w+)(/(\\d+))?");
+  private static final Pattern METHOD_PATTERN = Pattern.compile("(\\w+)(/(\\d+))?");
+
+  private BlazeCppTestLocator() {}
+
+  @Override
+  public List<Location> getLocation(
+      String protocol, String path, Project project, GlobalSearchScope scope) {
+    GoogleTestLocation location = null;
+    if (SmRunnerUtils.GENERIC_SUITE_PROTOCOL.equals(protocol)) {
+      location = getLocation(project, path, null);
+    } else if (SmRunnerUtils.GENERIC_TEST_PROTOCOL.equals(protocol)) {
+      String[] components = path.split(SmRunnerUtils.TEST_NAME_PARTS_SPLITTER);
+      location = components.length != 2 ? null : getLocation(project, components[0], components[1]);
+    }
+    return location != null ? ImmutableList.of(location) : ImmutableList.of();
+  }
+
+  @Nullable
+  private static GoogleTestLocation getLocation(
+      Project project, String suiteComponent, @Nullable String methodComponent) {
+    Matcher matcher = SUITE_PATTERN.matcher(suiteComponent);
+    if (!matcher.matches()) {
+      return null;
+    }
+    String instantiation = matcher.group(2);
+    String suite = matcher.group(3);
+    String method = null;
+    if (methodComponent != null) {
+      matcher = METHOD_PATTERN.matcher(methodComponent);
+      if (!matcher.matches()) {
+        return null;
+      }
+      method = matcher.group(1);
+    }
+    PsiElement psi = findPsiElement(project, instantiation, suite, method);
+    if (psi == null) {
+      return null;
+    }
+    GoogleTestSpecification gtest =
+        new GoogleTestSpecification.FromGtestOutput(suiteComponent, methodComponent);
+    return new GoogleTestLocation(psi, gtest);
+  }
+
+  @Nullable
+  private static PsiElement findPsiElement(
+      Project project,
+      @Nullable String instantiation,
+      @Nullable String suite,
+      @Nullable String method) {
+    if (suite == null) {
+      return null;
+    }
+    OCSymbol<?> symbol;
+    if (method != null) {
+      symbol = CidrGoogleTestUtilAdapter.findGoogleTestSymbol(project, suite, method);
+    } else if (instantiation != null) {
+      symbol =
+          CidrGoogleTestUtilAdapter.findGoogleTestInstantiationSymbol(
+              project, suite, instantiation);
+    } else {
+      symbol = findSuiteSymbol(project, suite);
+    }
+    if (symbol == null) {
+      return null;
+    }
+    PsiElement psi = symbol.locateDefinition();
+    while (!(psi instanceof OCStruct || psi instanceof OCMacroCall) && psi != null) {
+      PsiElement prev = psi.getPrevSibling();
+      psi = prev == null ? psi.getParent() : prev;
+    }
+    return psi;
+  }
+
+  @Nullable
+  private static OCStructSymbol findSuiteSymbol(Project project, String suite) {
+    CommonProcessors.FindProcessor<OCSymbol> processor =
+        new CommonProcessors.FindProcessor<OCSymbol>() {
+          @Override
+          protected boolean accept(OCSymbol symbol) {
+            return symbol instanceof OCStructSymbol
+                && CidrGoogleTestUtilAdapter.isGoogleTestClass((OCStructSymbol) symbol);
+          }
+        };
+    OCGlobalProjectSymbolsCache.processTopLevelAndMemberSymbols(project, processor, suite);
+    if (processor.isFound()) {
+      return (OCStructSymbol) processor.getFoundValue();
+    }
+    Collection<OCStructSymbol> symbolsForSuite =
+        CidrGoogleTestUtilAdapter.findGoogleTestSymbolsForSuiteSorted(project, suite);
+    return Iterables.getFirst(symbolsForSuite, null);
+  }
+}
diff --git a/clwb/src/com/google/idea/blaze/clwb/run/test/GoogleTestLocation.java b/clwb/src/com/google/idea/blaze/clwb/run/test/GoogleTestLocation.java
new file mode 100644
index 0000000..84e036a
--- /dev/null
+++ b/clwb/src/com/google/idea/blaze/clwb/run/test/GoogleTestLocation.java
@@ -0,0 +1,175 @@
+/*
+ * Copyright 2017 The Bazel Authors. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.idea.blaze.clwb.run.test;
+
+import com.google.idea.blaze.base.command.BlazeFlags;
+import com.google.idea.sdkcompat.cidr.CidrGoogleTestUtilAdapter;
+import com.intellij.execution.Location;
+import com.intellij.execution.PsiLocation;
+import com.intellij.openapi.util.Couple;
+import com.intellij.psi.PsiElement;
+import com.intellij.psi.util.PsiTreeUtil;
+import com.jetbrains.cidr.lang.psi.OCFile;
+import com.jetbrains.cidr.lang.psi.OCFunctionDefinition;
+import com.jetbrains.cidr.lang.psi.OCMacroCall;
+import com.jetbrains.cidr.lang.psi.OCMacroCallArgument;
+import com.jetbrains.cidr.lang.psi.OCStruct;
+import com.jetbrains.cidr.lang.symbols.OCSymbol;
+import com.jetbrains.cidr.lang.symbols.cpp.OCFunctionSymbol;
+import com.jetbrains.cidr.lang.symbols.cpp.OCStructSymbol;
+import com.jetbrains.cidr.lang.symbols.cpp.OCSymbolWithQualifiedName;
+import java.util.Collection;
+import java.util.List;
+import javax.annotation.Nullable;
+
+/** A {@link PsiLocation} with corresponding gtest specification */
+public class GoogleTestLocation extends PsiLocation<PsiElement> {
+
+  public final GoogleTestSpecification gtest;
+  @Nullable public final String testFilter;
+
+  GoogleTestLocation(PsiElement psi, GoogleTestSpecification gtest) {
+    super(psi);
+    this.gtest = gtest;
+    this.testFilter = gtest.testFilter();
+  }
+
+  /** The raw test filter string with '--test_filter=' prepended, or null if there is no filter. */
+  @Nullable
+  public String getTestFilterFlag() {
+    return testFilter != null ? BlazeFlags.TEST_FILTER + "=" + testFilter : null;
+  }
+
+  @Nullable
+  public static GoogleTestLocation findGoogleTest(Location<?> location) {
+    if (location instanceof GoogleTestLocation) {
+      return (GoogleTestLocation) location;
+    }
+    return findGoogleTest(location.getPsiElement());
+  }
+
+  @Nullable
+  public static GoogleTestLocation findGoogleTest(PsiElement element) {
+    // Copied from on CidrGoogleTestRunConfigurationProducer::findTestObject.
+    // Precedence order (decreasing): class/function, macro, file
+    PsiElement parent =
+        PsiTreeUtil.getNonStrictParentOfType(element, OCFunctionDefinition.class, OCStruct.class);
+
+    OCStructSymbol parentSymbol;
+    if (parent instanceof OCStruct
+        && ((parentSymbol = ((OCStruct) parent).getSymbol()) != null)
+        && CidrGoogleTestUtilAdapter.isGoogleTestClass(parentSymbol)) {
+      Couple<String> name = CidrGoogleTestUtilAdapter.extractGoogleTestName(parentSymbol);
+      if (name != null) {
+        return createFromClassAndMethod(parent, name.first, name.second);
+      }
+      String className = parentSymbol.getQualifiedName().getName();
+      return createFromClass(parent, className);
+    } else if (parent instanceof OCFunctionDefinition) {
+      OCFunctionSymbol symbol = ((OCFunctionDefinition) parent).getSymbol();
+      if (symbol != null) {
+        OCSymbolWithQualifiedName<?> resolvedOwner = symbol.getResolvedOwner();
+        if (resolvedOwner != null) {
+          OCSymbol<?> owner = resolvedOwner.getDefinitionSymbol();
+          if (owner instanceof OCStructSymbol
+              && CidrGoogleTestUtilAdapter.isGoogleTestClass((OCStructSymbol) owner)) {
+            OCStruct struct = (OCStruct) owner.locateDefinition();
+            Couple<String> name =
+                CidrGoogleTestUtilAdapter.extractGoogleTestName((OCStructSymbol) owner);
+            if (name != null) {
+              return createFromClassAndMethod(struct, name.first, name.second);
+            }
+            return createFromClass(struct, ((OCStructSymbol) owner).getQualifiedName().getName());
+          }
+        }
+      }
+    }
+
+    // if we're still here, let's test for a macro and, as a last resort, a file.
+    parent = PsiTreeUtil.getNonStrictParentOfType(element, OCMacroCall.class, OCFile.class);
+    if (parent instanceof OCMacroCall) {
+      OCMacroCall gtestMacro = CidrGoogleTestUtilAdapter.findGoogleTestMacros(parent);
+      if (gtestMacro != null) {
+        List<OCMacroCallArgument> arguments = gtestMacro.getArguments();
+        if (arguments.size() >= 2) {
+          OCMacroCallArgument suiteArg = arguments.get(0);
+          OCMacroCallArgument testArg = arguments.get(1);
+
+          // if the element is the first argument of macro call,
+          // then running entire suite, otherwise only a current test
+          boolean isSuite =
+              isFirstArgument(PsiTreeUtil.getParentOfType(element, OCMacroCallArgument.class))
+                  || isFirstArgument(element.getPrevSibling());
+          String suiteName = CidrGoogleTestUtilAdapter.extractArgumentValue(suiteArg);
+          String testName = CidrGoogleTestUtilAdapter.extractArgumentValue(testArg);
+          OCStructSymbol symbol =
+              CidrGoogleTestUtilAdapter.findGoogleTestSymbol(
+                  element.getProject(), suiteName, testName);
+          if (symbol != null) {
+            OCStruct targetElement = (OCStruct) symbol.locateDefinition();
+            return createFromClassAndMethod(targetElement, suiteName, isSuite ? null : testName);
+          }
+        }
+      }
+      Couple<String> suite = CidrGoogleTestUtilAdapter.extractFullSuiteNameFromMacro(parent);
+      if (suite != null) {
+        Collection<OCStructSymbol> res =
+            CidrGoogleTestUtilAdapter.findGoogleTestSymbolsForSuiteRandomly(
+                element.getProject(), suite.first, true);
+        if (res.size() != 0) {
+          OCStruct struct = (OCStruct) res.iterator().next().locateDefinition();
+          GoogleTestSpecification gtest =
+              new GoogleTestSpecification.FromPsiElement(suite.first, null, suite.second, null);
+          return new GoogleTestLocation(struct, gtest);
+        }
+      }
+    } else if (parent instanceof OCFile) {
+      return createFromFile(parent);
+    }
+    return null;
+  }
+
+  private static boolean isFirstArgument(@Nullable PsiElement element) {
+    OCMacroCall macroCall = PsiTreeUtil.getParentOfType(element, OCMacroCall.class);
+    if (macroCall != null) {
+      List<OCMacroCallArgument> arguments = macroCall.getArguments();
+      return arguments.size() > 0 && arguments.get(0).equals(element);
+    }
+    return false;
+  }
+
+  @Nullable
+  private static GoogleTestLocation createFromFile(@Nullable PsiElement element) {
+    return createFromClassAndMethod(element, null, null);
+  }
+
+  @Nullable
+  private static GoogleTestLocation createFromClass(
+      @Nullable PsiElement element, @Nullable String className) {
+    return createFromClassAndMethod(element, className, null);
+  }
+
+  @Nullable
+  private static GoogleTestLocation createFromClassAndMethod(
+      @Nullable PsiElement element, @Nullable String classOrSuiteName, @Nullable String testName) {
+    if (element == null) {
+      return null;
+    }
+    GoogleTestSpecification gtest =
+        new GoogleTestSpecification.FromPsiElement(classOrSuiteName, testName, null, null);
+    return new GoogleTestLocation(element, gtest);
+  }
+}
diff --git a/clwb/src/com/google/idea/blaze/clwb/run/test/GoogleTestSpecification.java b/clwb/src/com/google/idea/blaze/clwb/run/test/GoogleTestSpecification.java
new file mode 100644
index 0000000..2111410
--- /dev/null
+++ b/clwb/src/com/google/idea/blaze/clwb/run/test/GoogleTestSpecification.java
@@ -0,0 +1,122 @@
+/*
+ * Copyright 2017 The Bazel Authors. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.idea.blaze.clwb.run.test;
+
+import com.google.common.base.Joiner;
+import com.intellij.openapi.util.text.StringUtil;
+import javax.annotation.Nullable;
+
+/** A single gtest test case specification (https://github.com/google/googletest). */
+public interface GoogleTestSpecification {
+
+  /** The gtest filter string. Returns null if there is no filtering. */
+  @Nullable
+  String testFilter();
+
+  /** A human-readable description for this test. Returns null if there is no filtering. */
+  @Nullable
+  String description();
+
+  /**
+   * Built from the raw gtest output, without separating parameter components, etc.<br>
+   * This means there is no ambiguity -- guaranteed to be exactly what the gtest runner expects for
+   * this test case.
+   */
+  class FromGtestOutput implements GoogleTestSpecification {
+
+    private final String suiteComponent;
+    @Nullable private final String methodComponent;
+
+    public FromGtestOutput(String suiteComponent, @Nullable String methodComponent) {
+      this.suiteComponent = suiteComponent;
+      this.methodComponent = methodComponent;
+    }
+
+    @Override
+    public String testFilter() {
+      String method = methodComponent != null ? methodComponent : "*";
+      return String.format("%s.%s", suiteComponent, method);
+    }
+
+    @Override
+    public String description() {
+      return methodComponent == null
+          ? suiteComponent
+          : String.format("%s.%s", suiteComponent, methodComponent);
+    }
+  }
+
+  /**
+   * We don't know whether it's parameterized / typed in this context, so need to provide a more
+   * flexible filter.
+   */
+  class FromPsiElement implements GoogleTestSpecification {
+    @Nullable private final String suiteOrClass;
+    @Nullable private final String method;
+    @Nullable private final String instantiation;
+    @Nullable private final String param;
+
+    public FromPsiElement(
+        @Nullable String suiteOrClass,
+        @Nullable String method,
+        @Nullable String instantiation,
+        @Nullable String param) {
+      this.suiteOrClass = suiteOrClass;
+      this.method = method;
+      this.instantiation = instantiation;
+      this.param = param;
+    }
+
+    @Override
+    @Nullable
+    public String testFilter() {
+      if (suiteOrClass == null) {
+        return null;
+      }
+      String method = StringUtil.notNullize(this.method, "*");
+      String param = StringUtil.notNullize(this.param, "*");
+      if (instantiation != null) {
+        return Joiner.on(':')
+            .join(
+                String.format("%s/%s.%s/%s", instantiation, suiteOrClass, method, param),
+                String.format("%s/%s/%s.%s", instantiation, suiteOrClass, param, method));
+      }
+      // we don't know whether it's parameterized and/or typed, so need to handle all cases
+      return Joiner.on(':')
+          .join(
+              String.format("%s.%s", suiteOrClass, method),
+              String.format("%s/%s.%s", suiteOrClass, param, method),
+              String.format("*/%s.%s/*", suiteOrClass, method),
+              String.format("*/%s/*.%s", suiteOrClass, method));
+    }
+
+    @Override
+    @Nullable
+    public String description() {
+      if (suiteOrClass == null) {
+        return null;
+      }
+      if (method == null) {
+        return suiteOrClass;
+      }
+      if (instantiation == null) {
+        return suiteOrClass + "." + method;
+      }
+      String param = StringUtil.notNullize(this.param, "*");
+      return String.format("%s/%s.%s/%s", instantiation, suiteOrClass, method, param);
+    }
+  }
+}
diff --git a/clwb/src/com/google/idea/blaze/clwb/wizard2/BlazeEditProjectViewImportWizardStep.java b/clwb/src/com/google/idea/blaze/clwb/wizard2/BlazeEditProjectViewImportWizardStep.java
index 2998d4a..9c974de 100644
--- a/clwb/src/com/google/idea/blaze/clwb/wizard2/BlazeEditProjectViewImportWizardStep.java
+++ b/clwb/src/com/google/idea/blaze/clwb/wizard2/BlazeEditProjectViewImportWizardStep.java
@@ -82,6 +82,7 @@
   public void onWizardFinished() throws CommitStepException {
     try {
       getProjectBuilder().commit();
+      control.commit();
     } catch (BlazeProjectCommitException e) {
       throw new CommitStepException(e.getMessage());
     }
@@ -89,6 +90,6 @@
 
   @Override
   public String getHelpId() {
-    return "docs/project-views.md";
+    return "docs/project-views";
   }
 }
diff --git a/clwb/src/com/google/idea/blaze/clwb/wizard2/BlazeSelectBuildSystemBinaryStep.java b/clwb/src/com/google/idea/blaze/clwb/wizard2/BlazeSelectBuildSystemBinaryStep.java
index 808fb63..6797e1e 100644
--- a/clwb/src/com/google/idea/blaze/clwb/wizard2/BlazeSelectBuildSystemBinaryStep.java
+++ b/clwb/src/com/google/idea/blaze/clwb/wizard2/BlazeSelectBuildSystemBinaryStep.java
@@ -28,8 +28,7 @@
 import javax.swing.JPanel;
 import org.jetbrains.annotations.NotNull;
 
-class BlazeSelectBuildSystemBinaryStep extends
-    com.google.idea.blaze.clwb.wizard2.ProjectImportWizardStep {
+class BlazeSelectBuildSystemBinaryStep extends ProjectImportWizardStep {
 
   private final JPanel component = new JPanel(new BorderLayout());
   private SelectBazelBinaryControl control;
diff --git a/clwb/src/com/google/idea/blaze/clwb/wizard2/BlazeSelectProjectViewImportWizardStep.java b/clwb/src/com/google/idea/blaze/clwb/wizard2/BlazeSelectProjectViewImportWizardStep.java
index 9c99539..720091b 100644
--- a/clwb/src/com/google/idea/blaze/clwb/wizard2/BlazeSelectProjectViewImportWizardStep.java
+++ b/clwb/src/com/google/idea/blaze/clwb/wizard2/BlazeSelectProjectViewImportWizardStep.java
@@ -76,6 +76,6 @@
 
   @Override
   public String getHelpId() {
-    return "docs/project-views.md";
+    return "docs/project-views";
   }
 }
diff --git a/clwb/src/com/google/idea/blaze/clwb/wizard2/BlazeSelectWorkspaceImportWizardStep.java b/clwb/src/com/google/idea/blaze/clwb/wizard2/BlazeSelectWorkspaceImportWizardStep.java
index 2b1666e..3d719bb 100644
--- a/clwb/src/com/google/idea/blaze/clwb/wizard2/BlazeSelectWorkspaceImportWizardStep.java
+++ b/clwb/src/com/google/idea/blaze/clwb/wizard2/BlazeSelectWorkspaceImportWizardStep.java
@@ -25,8 +25,7 @@
 import javax.swing.JPanel;
 import org.jetbrains.annotations.NotNull;
 
-class BlazeSelectWorkspaceImportWizardStep extends
-    com.google.idea.blaze.clwb.wizard2.ProjectImportWizardStep {
+class BlazeSelectWorkspaceImportWizardStep extends ProjectImportWizardStep {
 
   private final JPanel component = new JPanel(new BorderLayout());
   private BlazeSelectWorkspaceControl control;
@@ -75,6 +74,6 @@
 
   @Override
   public String getHelpId() {
-    return "docs/import-project.md";
+    return "docs/import-project";
   }
 }
diff --git a/clwb/src/com/google/idea/blaze/plugin/CMakeOpenProjectActionOverride.java b/clwb/src/com/google/idea/blaze/plugin/CMakeOpenProjectActionOverride.java
new file mode 100644
index 0000000..8eb2d4e
--- /dev/null
+++ b/clwb/src/com/google/idea/blaze/plugin/CMakeOpenProjectActionOverride.java
@@ -0,0 +1,68 @@
+/*
+ * Copyright 2017 The Bazel Authors. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.idea.blaze.plugin;
+
+import com.intellij.ide.IdeBundle;
+import com.intellij.ide.actions.OpenProjectFileChooserDescriptor;
+import com.intellij.ide.impl.ProjectUtil;
+import com.intellij.openapi.actionSystem.AnActionEvent;
+import com.intellij.openapi.actionSystem.CommonDataKeys;
+import com.intellij.openapi.application.ReadAction;
+import com.intellij.openapi.fileChooser.FileChooser;
+import com.intellij.openapi.fileChooser.FileChooserDescriptor;
+import com.intellij.openapi.project.Project;
+import com.intellij.openapi.ui.Messages;
+import com.intellij.openapi.vfs.VfsUtil;
+import com.intellij.openapi.vfs.VirtualFile;
+import com.intellij.project.ProjectKt;
+import com.jetbrains.cidr.cpp.OpenCPPProjectAction;
+import com.jetbrains.cidr.cpp.cmake.CMakeProjectOpenProcessor;
+
+/** Replace {@link OpenCPPProjectAction} with a version that supports non-CMake projects. */
+public class CMakeOpenProjectActionOverride extends OpenCPPProjectAction {
+
+  @Override
+  public void actionPerformed(AnActionEvent e) {
+    FileChooserDescriptor descriptor = new OpenProjectFileChooserDescriptor(true, false);
+    Project project = e.getData(CommonDataKeys.PROJECT);
+    FileChooser.chooseFiles(
+        descriptor,
+        project,
+        VfsUtil.getUserHomeDir(),
+        files -> openProjectFile(project, files.get(0)));
+  }
+
+  private static VirtualFile getProjectFile(VirtualFile virtualFile) {
+    return ReadAction.compute(
+        () -> {
+          VirtualFile cmakeFile = CMakeProjectOpenProcessor.findSupportedSubFile(virtualFile);
+          if (cmakeFile != null) {
+            return cmakeFile;
+          }
+          return ProjectKt.getProjectStoreDirectory(virtualFile);
+        });
+  }
+
+  private static void openProjectFile(Project project, VirtualFile file) {
+    VirtualFile projectFile = getProjectFile(file);
+    if (projectFile != null) {
+      ProjectUtil.openOrImport(projectFile.getPath(), null, false);
+      return;
+    }
+    String message = IdeBundle.message("error.dir.contains.no.project", file.getPresentableUrl());
+    Messages.showInfoMessage(project, message, IdeBundle.message("title.cannot.open.project"));
+  }
+}
diff --git a/clwb/src/com/google/idea/blaze/plugin/CMakeWorkspaceOverride.java b/clwb/src/com/google/idea/blaze/plugin/CMakeWorkspaceOverride.java
new file mode 100644
index 0000000..7a2c2ca
--- /dev/null
+++ b/clwb/src/com/google/idea/blaze/plugin/CMakeWorkspaceOverride.java
@@ -0,0 +1,71 @@
+/*
+ * Copyright 2017 The Bazel Authors. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.idea.blaze.plugin;
+
+import com.google.idea.blaze.base.settings.Blaze;
+import com.intellij.openapi.module.Module;
+import com.intellij.openapi.module.ModuleManager;
+import com.intellij.openapi.module.ModuleType;
+import com.intellij.openapi.project.Project;
+import com.intellij.openapi.roots.ModuleRootManager;
+import com.intellij.openapi.roots.impl.storage.ClasspathStorage;
+import com.jetbrains.cidr.cpp.CPPModuleType;
+import com.jetbrains.cidr.cpp.cmake.workspace.CMakeWorkspace;
+import java.util.Arrays;
+import java.util.List;
+import java.util.stream.Collectors;
+
+/**
+ * Suppress {@link CMakeWorkspace#projectOpened} for non-CMake projects. Remove this if the <a
+ * href="https://youtrack.jetbrains.com/issue/CPP-9632">upstream bug</a> is fixed.
+ */
+public class CMakeWorkspaceOverride extends CMakeWorkspace {
+
+  private final boolean isBlazeProject;
+
+  public CMakeWorkspaceOverride(Project project) {
+    super(project);
+    isBlazeProject = Blaze.isBlazeProject(project);
+  }
+
+  @Override
+  public void projectOpened() {
+    if (!isBlazeProject) {
+      super.projectOpened();
+      return;
+    }
+    removeClasspathStorageFromModules(myProject);
+  }
+
+  /**
+   * A hacky way of removing the classpath ID. {@link ClasspathStorage} doesn't have a method for
+   * removing the existing storage type, but #setStorageType will silently do this if it's passed an
+   * unrecognized type.
+   */
+  private static void removeClasspathStorageFromModules(Project project) {
+    String dummyClasspathId = "classpath.id.which.does.not.exist";
+    for (Module cppModule : getCppModules(project)) {
+      ClasspathStorage.setStorageType(ModuleRootManager.getInstance(cppModule), dummyClasspathId);
+    }
+  }
+
+  private static List<Module> getCppModules(Project project) {
+    ModuleType<?> type = CPPModuleType.getInstance();
+    return Arrays.stream(ModuleManager.getInstance(project).getModules())
+        .filter(module -> type.equals(ModuleType.get(module)))
+        .collect(Collectors.toList());
+  }
+}
diff --git a/clwb/src/com/google/idea/blaze/plugin/ClwbSpecificInitializer.java b/clwb/src/com/google/idea/blaze/plugin/ClwbSpecificInitializer.java
index 4a3445e..75b3b7b 100644
--- a/clwb/src/com/google/idea/blaze/plugin/ClwbSpecificInitializer.java
+++ b/clwb/src/com/google/idea/blaze/plugin/ClwbSpecificInitializer.java
@@ -16,12 +16,8 @@
 package com.google.idea.blaze.plugin;
 
 import com.google.idea.blaze.base.plugin.BlazeActionRemover;
+import com.google.idea.sdkcompat.clion.CMakeActionList;
 import com.intellij.openapi.components.ApplicationComponent;
-import com.jetbrains.cidr.cpp.cmake.actions.ChangeCMakeProjectContentRootAction;
-import com.jetbrains.cidr.cpp.cmake.actions.DropCMakeCacheAction;
-import com.jetbrains.cidr.cpp.cmake.actions.OpenCMakeSettingsAction;
-import com.jetbrains.cidr.cpp.cmake.actions.ReloadCMakeProjectAction;
-import com.jetbrains.cidr.cpp.cmake.actions.ToggleCMakeAutoReloadAction;
 
 /** Runs on startup. */
 public class ClwbSpecificInitializer extends ApplicationComponent.Adapter {
@@ -33,12 +29,9 @@
 
   // The original actions will be visible only on plain IDEA projects.
   private static void hideCMakeActions() {
-    BlazeActionRemover.hideAction(ChangeCMakeProjectContentRootAction.ID);
-    BlazeActionRemover.hideAction(DropCMakeCacheAction.ID);
-    BlazeActionRemover.hideAction(OpenCMakeSettingsAction.ID);
-    BlazeActionRemover.hideAction(ReloadCMakeProjectAction.ID);
-    BlazeActionRemover.hideAction(ToggleCMakeAutoReloadAction.ID);
-    // 'CMake' > 'Show Generated CMake Files' action
-    BlazeActionRemover.hideAction("CMake.ShowGeneratedDir");
+    for (String actionId : CMakeActionList.CMAKE_ACTION_IDS) {
+      BlazeActionRemover.hideAction(actionId);
+    }
+    BlazeActionRemover.replaceAction("OpenCPPProject", new CMakeOpenProjectActionOverride());
   }
 }
diff --git a/common/actionhelper/src/com/google/idea/common/actionhelper/ActionPresentationHelper.java b/common/actionhelper/src/com/google/idea/common/actionhelper/ActionPresentationHelper.java
index 69c5e1e..048b0f1 100644
--- a/common/actionhelper/src/com/google/idea/common/actionhelper/ActionPresentationHelper.java
+++ b/common/actionhelper/src/com/google/idea/common/actionhelper/ActionPresentationHelper.java
@@ -15,22 +15,30 @@
  */
 package com.google.idea.common.actionhelper;
 
+import com.google.common.collect.Iterables;
+import com.intellij.openapi.actionSystem.ActionPlaces;
 import com.intellij.openapi.actionSystem.AnActionEvent;
 import com.intellij.openapi.actionSystem.Presentation;
 import com.intellij.openapi.vfs.VirtualFile;
-import java.util.List;
+import java.util.Collection;
 import javax.annotation.Nullable;
 
 /** Helps setting the presentation enabled/visible/text states. */
 public class ActionPresentationHelper {
 
-  private final Presentation presentation;
+  private final AnActionEvent event;
+
   private boolean enabled = true;
   private boolean visible = true;
   private boolean disableWithoutSubject;
+  private boolean hideInContextMenuIfDisabled;
+
   private boolean hasSubject;
   private String text;
   private String subjectText;
+  private boolean useTextMnemonic;
+
+  private String description;
 
   /** Converts a subject to a string */
   @FunctionalInterface
@@ -38,12 +46,12 @@
     String subjectToString(T subject);
   }
 
-  private ActionPresentationHelper(Presentation presentation) {
-    this.presentation = presentation;
+  private ActionPresentationHelper(AnActionEvent event) {
+    this.event = event;
   }
 
-  public static ActionPresentationHelper of(AnActionEvent e) {
-    return new ActionPresentationHelper(e.getPresentation());
+  public static ActionPresentationHelper of(AnActionEvent event) {
+    return new ActionPresentationHelper(event);
   }
 
   /** Disables the action if the condition is true. */
@@ -52,12 +60,17 @@
     return this;
   }
 
-  /** Hides the action if the condition is true. */
+  /** Hides the action if the condition is true. If the action is hidden, it is also disabled. */
   public ActionPresentationHelper hideIf(boolean hideCondition) {
     this.visible = this.visible && !hideCondition;
     return this;
   }
 
+  public ActionPresentationHelper hideInContextMenuIfDisabled() {
+    this.hideInContextMenuIfDisabled = true;
+    return this;
+  }
+
   /** Disables the action if no subject has been provided. */
   public ActionPresentationHelper disableWithoutSubject() {
     this.disableWithoutSubject = true;
@@ -70,6 +83,18 @@
     return this;
   }
 
+  /** Use & or _ in the presentation text as a mnemonic shortcut. */
+  public ActionPresentationHelper useTextMnemonic() {
+    this.useTextMnemonic = true;
+    return this;
+  }
+
+  /** Sets the description of the presentation. */
+  public ActionPresentationHelper setDescription(String description) {
+    this.description = description;
+    return this;
+  }
+
   /**
    * Sets the text depending on the subject.
    *
@@ -121,7 +146,7 @@
       String noSubjectText,
       String singleSubjectText,
       String multipleSubjectText,
-      List<VirtualFile> files) {
+      Collection<VirtualFile> files) {
     return setTextWithSubjects(
         noSubjectText,
         singleSubjectText,
@@ -142,7 +167,7 @@
       String noSubjectText,
       String singleSubjectText,
       String multipleSubjectText,
-      List<T> subjects,
+      Collection<T> subjects,
       SubjectToString<T> subjectToString) {
     if (subjects.size() > 1) {
       this.text = noSubjectText;
@@ -150,7 +175,7 @@
       this.hasSubject = true;
       return this;
     } else {
-      T subject = !subjects.isEmpty() ? subjects.get(0) : null;
+      T subject = !subjects.isEmpty() ? Iterables.getOnlyElement(subjects) : null;
       return setTextWithSubject(noSubjectText, singleSubjectText, subject, subjectToString);
     }
   }
@@ -160,16 +185,26 @@
   }
 
   public void commit() {
-    boolean enabled = this.enabled;
+    boolean enabled = this.enabled && this.visible;
     if (disableWithoutSubject) {
       enabled = enabled && hasSubject;
     }
+    boolean visible = this.visible;
+    if (hideInContextMenuIfDisabled && !enabled && ActionPlaces.isPopupPlace(event.getPlace())) {
+      visible = false;
+    }
+
+    Presentation presentation = event.getPresentation();
     presentation.setEnabled(enabled);
     presentation.setVisible(visible);
 
     String text = enabled && hasSubject ? subjectText : this.text;
     if (text != null) {
-      presentation.setText(text, false);
+      presentation.setText(text, useTextMnemonic);
+    }
+
+    if (description != null) {
+      presentation.setDescription(description);
     }
   }
 }
diff --git a/common/experiments/src/com/google/idea/common/experiments/WebExperimentSyncer.java b/common/experiments/src/com/google/idea/common/experiments/WebExperimentSyncer.java
index 464a62d..1f1b6e0 100644
--- a/common/experiments/src/com/google/idea/common/experiments/WebExperimentSyncer.java
+++ b/common/experiments/src/com/google/idea/common/experiments/WebExperimentSyncer.java
@@ -96,7 +96,7 @@
     experimentValues = loadCache();
     ListenableFuture<String> response = executor.submit(new WebExperimentsDownloader());
     response.addListener(
-        new WebExperimentsResultProcessor(response), MoreExecutors.sameThreadExecutor());
+        new WebExperimentsResultProcessor(response), MoreExecutors.directExecutor());
   }
 
   private void scheduleNextRefresh(boolean refreshWasSuccessful) {
@@ -105,7 +105,7 @@
     ListenableScheduledFuture<String> refreshResults =
         executor.schedule(new WebExperimentsDownloader(), delayInMinutes, TimeUnit.MINUTES);
     refreshResults.addListener(
-        new WebExperimentsResultProcessor(refreshResults), MoreExecutors.sameThreadExecutor());
+        new WebExperimentsResultProcessor(refreshResults), MoreExecutors.directExecutor());
   }
 
   private class WebExperimentsDownloader implements Callable<String> {
@@ -115,7 +115,7 @@
       logger.debug("About to fetch experiments.");
       return HttpRequests.request(
               System.getProperty(EXPERIMENTS_URL_PROPERTY, DEFAULT_EXPERIMENT_URL) + pluginName)
-          .readString(null /* progress indicator */);
+          .readString(/* progress indicator */ null);
     }
   }
 
@@ -144,10 +144,10 @@
         setExperimentValues(mapBuilder);
 
         logger.debug("Successfully fetched experiments: " + getExperimentValues());
-        scheduleNextRefresh(true /* refreshWasSuccessful */);
+        scheduleNextRefresh(/* refreshWasSuccessful */ true);
       } catch (InterruptedException | ExecutionException | RuntimeException e) {
         logger.debug("Error fetching experiments", e);
-        scheduleNextRefresh(false /* refreshWasSuccessful */);
+        scheduleNextRefresh(/* refreshWasSuccessful */ false);
       }
     }
   }
diff --git a/common/formatter/src/com/google/idea/common/formatter/DelegatingCodeStyleManager.java b/common/formatter/src/com/google/idea/common/formatter/DelegatingCodeStyleManager.java
index 354fea4..b5ab232 100644
--- a/common/formatter/src/com/google/idea/common/formatter/DelegatingCodeStyleManager.java
+++ b/common/formatter/src/com/google/idea/common/formatter/DelegatingCodeStyleManager.java
@@ -15,7 +15,7 @@
  */
 package com.google.idea.common.formatter;
 
-import com.google.idea.sdkcompat.codestyle.CodeStyleManagerSdkCompatAdapter;
+import com.google.idea.sdkcompat.codestyle.DelegatingCodeStyleManagerSdkCompatAdapter;
 import com.intellij.lang.ASTNode;
 import com.intellij.openapi.editor.Document;
 import com.intellij.openapi.fileTypes.FileType;
@@ -32,12 +32,11 @@
 import javax.annotation.Nullable;
 
 /** A delegating {@link CodeStyleManager}. */
-public abstract class DelegatingCodeStyleManager extends CodeStyleManagerSdkCompatAdapter {
-
-  private final CodeStyleManager delegate;
+public abstract class DelegatingCodeStyleManager
+    extends DelegatingCodeStyleManagerSdkCompatAdapter {
 
   public DelegatingCodeStyleManager(CodeStyleManager delegate) {
-    this.delegate = delegate;
+    super(delegate);
   }
 
   @Override
diff --git a/cpp/BUILD b/cpp/BUILD
index c157efc..bbf1c7e 100644
--- a/cpp/BUILD
+++ b/cpp/BUILD
@@ -1,18 +1,10 @@
 licenses(["notice"])  # Apache 2.0
 
-load("//intellij_platform_sdk:build_defs.bzl", "select_for_plugin_api")
-
 java_library(
     name = "cpp",
     srcs = glob(
         ["src/**/*.java"],
-        exclude = [
-            "src/com/google/idea/blaze/cpp/versioned/**",
-        ],
-    ) + select_for_plugin_api({
-        "android-studio-145.1617.8": [":api_v145_sources"],
-        "default": [":api_v162_sources"],
-    }),
+    ),
     visibility = ["//visibility:public"],
     deps = [
         "//base",
@@ -29,18 +21,6 @@
     visibility = ["//visibility:public"],
 )
 
-filegroup(
-    name = "api_v145_sources",
-    srcs = glob(["src/com/google/idea/blaze/cpp/versioned/v145/**"]),
-    visibility = ["//visibility:private"],
-)
-
-filegroup(
-    name = "api_v162_sources",
-    srcs = glob(["src/com/google/idea/blaze/cpp/versioned/v162/**"]),
-    visibility = ["//visibility:private"],
-)
-
 load(
     "//testing:test_defs.bzl",
     "intellij_unit_test_suite",
@@ -54,6 +34,7 @@
         ":cpp",
         "//base:unit_test_utils",
         "//intellij_platform_sdk:plugin_api_for_tests",
+        "//sdkcompat",
         "@jsr305_annotations//jar",
         "@junit//jar",
     ],
diff --git a/cpp/src/META-INF/blaze-cpp.xml b/cpp/src/META-INF/blaze-cpp.xml
index aa0fb37..158f6d8 100644
--- a/cpp/src/META-INF/blaze-cpp.xml
+++ b/cpp/src/META-INF/blaze-cpp.xml
@@ -17,6 +17,14 @@
   <depends>com.intellij.modules.cidr.lang</depends>
   <depends>com.intellij.modules.cidr.debugger</depends>
 
+  <application-components>
+    <component>
+      <implementation-class>
+        com.google.idea.blaze.cpp.CidrSymbolBuilderSuppressor
+      </implementation-class>
+    </component>
+  </application-components>
+
   <extensions defaultExtensionNs="com.google.idea.blaze">
     <SyncPlugin implementation="com.google.idea.blaze.cpp.BlazeCSyncPlugin"/>
     <PrefetchFileSource implementation="com.google.idea.blaze.cpp.CPrefetchFileSource"/>
diff --git a/cpp/src/com/google/idea/blaze/cpp/BlazeCSyncPlugin.java b/cpp/src/com/google/idea/blaze/cpp/BlazeCSyncPlugin.java
index 4168385..aa1736e 100644
--- a/cpp/src/com/google/idea/blaze/cpp/BlazeCSyncPlugin.java
+++ b/cpp/src/com/google/idea/blaze/cpp/BlazeCSyncPlugin.java
@@ -89,7 +89,7 @@
     VirtualFile execRoot =
         VirtualFileSystemProvider.getInstance()
             .getSystem()
-            .refreshAndFindFileByIoFile(blazeProjectData.blazeRoots.executionRoot);
+            .refreshAndFindFileByIoFile(blazeProjectData.blazeInfo.getExecutionRoot());
     if (execRoot != null) {
       VfsUtil.markDirtyAndRefresh(false, true, true, execRoot);
     }
diff --git a/cpp/src/com/google/idea/blaze/cpp/BlazeCWorkspace.java b/cpp/src/com/google/idea/blaze/cpp/BlazeCWorkspace.java
index abcb294..c0f5b94 100644
--- a/cpp/src/com/google/idea/blaze/cpp/BlazeCWorkspace.java
+++ b/cpp/src/com/google/idea/blaze/cpp/BlazeCWorkspace.java
@@ -19,37 +19,26 @@
 import com.google.common.collect.ImmutableList;
 import com.google.idea.blaze.base.model.BlazeProjectData;
 import com.google.idea.blaze.base.scope.BlazeContext;
-import com.google.idea.blaze.base.settings.Blaze;
+import com.google.idea.sdkcompat.cidr.OCWorkspaceAdapter;
 import com.intellij.openapi.components.ServiceManager;
 import com.intellij.openapi.diagnostic.Logger;
 import com.intellij.openapi.project.Project;
 import com.intellij.openapi.vfs.VirtualFile;
 import com.jetbrains.cidr.lang.symbols.OCSymbol;
 import com.jetbrains.cidr.lang.workspace.OCResolveConfiguration;
-import com.jetbrains.cidr.lang.workspace.OCWorkspace;
-import com.jetbrains.cidr.lang.workspace.OCWorkspaceModificationTrackers;
 import java.util.Collection;
 import java.util.List;
 import javax.annotation.Nullable;
 
 /** Main entry point for C/CPP configuration data. */
-public final class BlazeCWorkspace implements OCWorkspace {
+public final class BlazeCWorkspace extends OCWorkspaceAdapter {
   private static final Logger logger = Logger.getInstance(BlazeCWorkspace.class);
 
-  @Nullable private final Project project;
-  @Nullable private final OCWorkspaceModificationTrackers modTrackers;
-
-  @Nullable private BlazeConfigurationResolver configurationResolver;
+  private final BlazeConfigurationResolver configurationResolver;
 
   private BlazeCWorkspace(Project project) {
-    if (Blaze.isBlazeProject(project)) {
-      this.project = project;
-      this.modTrackers = new OCWorkspaceModificationTrackers(project);
-      this.configurationResolver = new BlazeConfigurationResolver(project);
-    } else {
-      this.project = null;
-      this.modTrackers = null;
-    }
+    super(project);
+    this.configurationResolver = new BlazeConfigurationResolver(project);
   }
 
   public static BlazeCWorkspace getInstance(Project project) {
@@ -57,13 +46,8 @@
   }
 
   public void update(BlazeContext context, BlazeProjectData blazeProjectData) {
-    logger.assertTrue(project != null);
-    logger.assertTrue(modTrackers != null);
-    logger.assertTrue(configurationResolver != null);
-
-    long start = System.currentTimeMillis();
-
     // Non-incremental update to our c configurations.
+    long start = System.currentTimeMillis();
     configurationResolver.update(context, blazeProjectData);
     long end = System.currentTimeMillis();
 
@@ -97,29 +81,15 @@
     return false;
   }
 
-  @Nullable
-  @Override
-  public OCResolveConfiguration getSelectedResolveConfiguration() {
-    return null;
-  }
-
-  @Override
-  public OCWorkspaceModificationTrackers getModificationTrackers() {
-    logger.assertTrue(modTrackers != null);
-    return modTrackers;
-  }
-
   @Override
   public List<? extends OCResolveConfiguration> getConfigurations() {
-    return configurationResolver == null
-        ? ImmutableList.of()
-        : configurationResolver.getAllConfigurations();
+    return configurationResolver.getAllConfigurations();
   }
 
   @Override
   public List<? extends OCResolveConfiguration> getConfigurationsForFile(
       @Nullable VirtualFile sourceFile) {
-    if (sourceFile == null || !sourceFile.isValid() || configurationResolver == null) {
+    if (sourceFile == null || !sourceFile.isValid()) {
       return ImmutableList.of();
     }
     OCResolveConfiguration config = configurationResolver.getConfigurationForFile(sourceFile);
diff --git a/cpp/src/com/google/idea/blaze/cpp/BlazeCompilerMacros.java b/cpp/src/com/google/idea/blaze/cpp/BlazeCompilerMacros.java
index 28eba21..654a8e2 100644
--- a/cpp/src/com/google/idea/blaze/cpp/BlazeCompilerMacros.java
+++ b/cpp/src/com/google/idea/blaze/cpp/BlazeCompilerMacros.java
@@ -15,7 +15,6 @@
  */
 package com.google.idea.blaze.cpp;
 
-import com.google.common.base.Joiner;
 import com.google.common.collect.ImmutableCollection;
 import com.google.common.collect.ImmutableMap;
 import com.google.common.collect.ImmutableSet;
@@ -62,17 +61,20 @@
 
     // Combine the info we got from Blaze with the info we get from IntelliJ's methods.
     ImmutableSet.Builder<String> allDefinesBuilder = ImmutableSet.builder();
-    // IntelliJ expects a string of "#define [VAR_NAME]\n#define [VAR_NAME2]\n..."
+    // IntelliJ expects a string of "#define [VAR_NAME] [VALUE]\n#define [VAR_NAME2] [VALUE]\n...",
+    // where VALUE is optional.
     for (String globalDefine : globalDefines) {
-      allDefinesBuilder.add("#define " + globalDefine);
-    }
-    if (compilerInfo != null) {
-      String[] split = compilerInfo.defines.split("\n");
-      for (String s : split) {
-        allDefinesBuilder.add(s);
+      String[] split = globalDefine.split("=", 2);
+      if (split.length == 1) {
+        allDefinesBuilder.add("#define " + split[0]);
+      } else {
+        allDefinesBuilder.add("#define " + split[0] + " " + split[1]);
       }
     }
-    final String allDefines = Joiner.on("\n").join(allDefinesBuilder.build());
+    String allDefines = String.join("\n", allDefinesBuilder.build());
+    if (compilerInfo != null) {
+      allDefines += "\n" + compilerInfo.defines;
+    }
 
     Map<String, String> allFeatures = Maps.newHashMap();
     allFeatures.putAll(globalFeatures);
diff --git a/cpp/src/com/google/idea/blaze/cpp/BlazeCompilerSettings.java b/cpp/src/com/google/idea/blaze/cpp/BlazeCompilerSettings.java
index 966b37c..ccc178d 100644
--- a/cpp/src/com/google/idea/blaze/cpp/BlazeCompilerSettings.java
+++ b/cpp/src/com/google/idea/blaze/cpp/BlazeCompilerSettings.java
@@ -15,21 +15,19 @@
  */
 package com.google.idea.blaze.cpp;
 
-import com.google.common.base.Joiner;
 import com.google.common.collect.ImmutableList;
 import com.google.idea.blaze.base.model.primitives.WorkspaceRoot;
+import com.google.idea.sdkcompat.cidr.CidrSwitchBuilderAdapter;
 import com.intellij.openapi.project.Project;
 import com.intellij.openapi.vfs.VirtualFile;
 import com.jetbrains.cidr.lang.OCLanguageKind;
 import com.jetbrains.cidr.lang.toolchains.CidrCompilerSwitches;
-import com.jetbrains.cidr.lang.toolchains.CidrSwitchBuilder;
 import com.jetbrains.cidr.lang.toolchains.CidrToolEnvironment;
 import com.jetbrains.cidr.lang.toolchains.DefaultCidrToolEnvironment;
 import com.jetbrains.cidr.lang.workspace.compiler.OCCompilerKind;
 import com.jetbrains.cidr.lang.workspace.compiler.OCCompilerSettings;
 import java.io.File;
 import java.util.List;
-import java.util.stream.Collectors;
 import javax.annotation.Nullable;
 
 final class BlazeCompilerSettings extends OCCompilerSettings {
@@ -56,7 +54,10 @@
 
   @Override
   public OCCompilerKind getCompiler(OCLanguageKind languageKind) {
-    return null;
+    if (languageKind == OCLanguageKind.C || languageKind == OCLanguageKind.CPP) {
+      return OCCompilerKind.CLANG;
+    }
+    return OCCompilerKind.UNKNOWN;
   }
 
   @Override
@@ -89,25 +90,10 @@
     if (lang == OCLanguageKind.CPP) {
       return cppCompilerSwitches;
     }
-    return new CidrSwitchBuilder().build();
+    return new CidrSwitchBuilderAdapter().build();
   }
 
   private static CidrCompilerSwitches getCompilerSwitches(List<String> allCompilerFlags) {
-    // Explanation of hack:
-    // - this list of switches is currently only used in one place -- GCCCompiler.tryRunGCC.
-    // - list is written to an argument file, whitespace-separated, then passed as a @file arg to
-    // clang.
-    // In this context, escaped whitespace within a single arg is not handled.
-    // Currently, the only way (short of using reflection) to ensure unescaped whitespace
-    // is to have CidrSwitchBuilder treat whitespace as a delimiter between args.
-    allCompilerFlags =
-        allCompilerFlags
-            .stream()
-            .map(flag -> flag.replace("\\ ", " "))
-            .collect(Collectors.toList());
-
-    return new CidrSwitchBuilder()
-        .addAll(Joiner.on(" ").join(allCompilerFlags), CidrSwitchBuilder.Format.FILE_ARGS)
-        .build();
+    return new CidrSwitchBuilderAdapter().addAllRaw(allCompilerFlags).build();
   }
 }
diff --git a/cpp/src/com/google/idea/blaze/cpp/BlazeConfigurationResolver.java b/cpp/src/com/google/idea/blaze/cpp/BlazeConfigurationResolver.java
index 5e6e8b9..d01798e 100644
--- a/cpp/src/com/google/idea/blaze/cpp/BlazeConfigurationResolver.java
+++ b/cpp/src/com/google/idea/blaze/cpp/BlazeConfigurationResolver.java
@@ -17,15 +17,16 @@
 
 import static java.nio.charset.StandardCharsets.UTF_8;
 
+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.common.collect.Lists;
 import com.google.common.collect.Maps;
 import com.google.common.collect.Sets;
 import com.google.common.util.concurrent.Futures;
 import com.google.common.util.concurrent.ListenableFuture;
 import com.google.idea.blaze.base.async.executor.BlazeExecutor;
+import com.google.idea.blaze.base.command.info.BlazeInfo;
 import com.google.idea.blaze.base.ideinfo.CToolchainIdeInfo;
 import com.google.idea.blaze.base.ideinfo.TargetIdeInfo;
 import com.google.idea.blaze.base.ideinfo.TargetKey;
@@ -33,12 +34,15 @@
 import com.google.idea.blaze.base.io.FileAttributeProvider;
 import com.google.idea.blaze.base.model.BlazeProjectData;
 import com.google.idea.blaze.base.model.primitives.ExecutionRootPath;
+import com.google.idea.blaze.base.model.primitives.Kind;
 import com.google.idea.blaze.base.model.primitives.LanguageClass;
+import com.google.idea.blaze.base.model.primitives.WorkspaceRoot;
 import com.google.idea.blaze.base.scope.BlazeContext;
 import com.google.idea.blaze.base.scope.Scope;
 import com.google.idea.blaze.base.scope.ScopedFunction;
 import com.google.idea.blaze.base.scope.output.IssueOutput;
 import com.google.idea.blaze.base.scope.scopes.TimingScope;
+import com.google.idea.blaze.base.settings.Blaze;
 import com.google.idea.blaze.base.sync.workspace.ExecutionRootPathResolver;
 import com.google.idea.blaze.base.sync.workspace.WorkspacePathResolver;
 import com.google.idea.blaze.base.targetmaps.SourceToTargetMap;
@@ -49,15 +53,20 @@
 import com.intellij.openapi.vfs.VfsUtilCore;
 import com.intellij.openapi.vfs.VirtualFile;
 import com.jetbrains.cidr.lang.workspace.OCResolveConfiguration;
+import com.jetbrains.cidr.toolchains.CompilerInfoCache;
 import java.io.File;
 import java.io.IOException;
 import java.io.PrintWriter;
+import java.util.AbstractMap.SimpleImmutableEntry;
+import java.util.ArrayList;
+import java.util.LinkedHashSet;
 import java.util.List;
 import java.util.Map;
 import java.util.Set;
 import java.util.concurrent.Callable;
 import java.util.concurrent.ConcurrentMap;
 import java.util.concurrent.ExecutionException;
+import java.util.stream.Collectors;
 import javax.annotation.Nullable;
 
 final class BlazeConfigurationResolver {
@@ -83,15 +92,24 @@
 
   public void update(BlazeContext context, BlazeProjectData blazeProjectData) {
     ImmutableMap<TargetKey, CToolchainIdeInfo> toolchainLookupMap =
-        BlazeResolveConfiguration.buildToolchainLookupMap(
-            context, blazeProjectData.targetMap, blazeProjectData.reverseDependencies);
+        BlazeResolveConfiguration.buildToolchainLookupMap(context, blazeProjectData.targetMap);
     ImmutableMap<File, VirtualFile> headerRoots =
         collectHeaderRoots(context, blazeProjectData, toolchainLookupMap);
+    ImmutableMap<CToolchainIdeInfo, BlazeCompilerSettings> compilerSettings =
+        buildCompilerSettingsMap(
+            context, project, toolchainLookupMap, blazeProjectData.workspacePathResolver);
+    CompilerInfoCache compilerInfoCache = new CompilerInfoCache();
     resolveConfigurations =
-        buildBlazeConfigurationMap(context, blazeProjectData, toolchainLookupMap, headerRoots);
+        buildBlazeConfigurationMap(
+            context,
+            blazeProjectData,
+            toolchainLookupMap,
+            headerRoots,
+            compilerSettings,
+            compilerInfoCache);
   }
 
-  private static ImmutableMap<File, VirtualFile> collectHeaderRoots(
+  private ImmutableMap<File, VirtualFile> collectHeaderRoots(
       BlazeContext parentContext,
       BlazeProjectData blazeProjectData,
       ImmutableMap<TargetKey, CToolchainIdeInfo> toolchainLookupMap) {
@@ -107,10 +125,14 @@
             });
   }
 
-  private static ImmutableMap<File, VirtualFile> doCollectHeaderRoots(
+  private ImmutableMap<File, VirtualFile> doCollectHeaderRoots(
       BlazeContext context, BlazeProjectData projectData, Set<ExecutionRootPath> rootPaths) {
     ExecutionRootPathResolver pathResolver =
-        new ExecutionRootPathResolver(projectData.blazeRoots, projectData.workspacePathResolver);
+        new ExecutionRootPathResolver(
+            Blaze.getBuildSystem(project),
+            WorkspaceRoot.fromProject(project),
+            projectData.blazeInfo.getExecutionRoot(),
+            projectData.workspacePathResolver);
     ConcurrentMap<File, VirtualFile> rootsMap = Maps.newConcurrentMap();
     List<ListenableFuture<Void>> futures = Lists.newArrayListWithCapacity(rootPaths.size());
     for (ExecutionRootPath path : rootPaths) {
@@ -123,7 +145,7 @@
                   VirtualFile vf = getVirtualFile(file);
                   if (vf != null) {
                     rootsMap.put(file, vf);
-                  } else if (!projectData.blazeRoots.isOutputArtifact(path)
+                  } else if (!isOutputArtifact(projectData.blazeInfo, path)
                       && FileAttributeProvider.getInstance().exists(file)) {
                     // If it's not a blaze output file, we expect it to always resolve.
                     LOG.info(String.format("Unresolved header root %s", file.getAbsolutePath()));
@@ -145,17 +167,24 @@
     return ImmutableMap.of();
   }
 
+  private static boolean isOutputArtifact(BlazeInfo blazeInfo, ExecutionRootPath path) {
+    return ExecutionRootPath.isAncestor(blazeInfo.getBlazeGenfilesExecutionRootPath(), path, false)
+        || ExecutionRootPath.isAncestor(blazeInfo.getBlazeBinExecutionRootPath(), path, false);
+  }
+
   private static Set<ExecutionRootPath> collectExecutionRootPaths(
       TargetMap targetMap, ImmutableMap<TargetKey, CToolchainIdeInfo> toolchainLookupMap) {
     Set<ExecutionRootPath> paths = Sets.newHashSet();
     for (TargetIdeInfo target : targetMap.targets()) {
       if (target.cIdeInfo != null) {
+        paths.addAll(target.cIdeInfo.localIncludeDirectories);
         paths.addAll(target.cIdeInfo.transitiveSystemIncludeDirectories);
         paths.addAll(target.cIdeInfo.transitiveIncludeDirectories);
         paths.addAll(target.cIdeInfo.transitiveQuoteIncludeDirectories);
       }
     }
-    for (CToolchainIdeInfo toolchain : toolchainLookupMap.values()) {
+    Set<CToolchainIdeInfo> toolchains = new LinkedHashSet<>(toolchainLookupMap.values());
+    for (CToolchainIdeInfo toolchain : toolchains) {
       paths.addAll(toolchain.builtInIncludeDirectories);
       paths.addAll(toolchain.unfilteredToolchainSystemIncludes);
     }
@@ -176,7 +205,9 @@
       BlazeContext parentContext,
       BlazeProjectData blazeProjectData,
       ImmutableMap<TargetKey, CToolchainIdeInfo> toolchainLookupMap,
-      ImmutableMap<File, VirtualFile> headerRoots) {
+      ImmutableMap<File, VirtualFile> headerRoots,
+      ImmutableMap<CToolchainIdeInfo, BlazeCompilerSettings> compilerSettings,
+      CompilerInfoCache compilerInfoCache) {
     // Type specification needed to avoid incorrect type inference during command line build.
     return Scope.push(
         parentContext,
@@ -184,11 +215,11 @@
             context -> {
               context.push(new TimingScope("Build C configuration map"));
 
-              ConcurrentMap<CToolchainIdeInfo, File> compilerWrapperCache = Maps.newConcurrentMap();
               List<ListenableFuture<MapEntry>> mapEntryFutures = Lists.newArrayList();
 
               for (TargetIdeInfo target : blazeProjectData.targetMap.targets()) {
-                if (target.kind.getLanguageClass() == LanguageClass.C) {
+                if (target.kind.getLanguageClass() == LanguageClass.C
+                    && target.kind != Kind.CC_TOOLCHAIN) {
                   ListenableFuture<MapEntry> future =
                       submit(
                           () ->
@@ -196,8 +227,9 @@
                                   target,
                                   toolchainLookupMap,
                                   headerRoots,
-                                  compilerWrapperCache,
-                                  blazeProjectData));
+                                  compilerSettings,
+                                  blazeProjectData,
+                                  compilerInfoCache));
                   mapEntryFutures.add(future);
                 }
               }
@@ -236,64 +268,116 @@
       TargetIdeInfo target,
       ImmutableMap<TargetKey, CToolchainIdeInfo> toolchainLookupMap,
       ImmutableMap<File, VirtualFile> headerRoots,
-      ConcurrentMap<CToolchainIdeInfo, File> compilerWrapperCache,
-      BlazeProjectData blazeProjectData) {
+      ImmutableMap<CToolchainIdeInfo, BlazeCompilerSettings> compilerSettingsMap,
+      BlazeProjectData blazeProjectData,
+      CompilerInfoCache compilerInfoCache) {
     TargetKey targetKey = target.key;
-
     CToolchainIdeInfo toolchainIdeInfo = toolchainLookupMap.get(targetKey);
-    if (toolchainIdeInfo != null) {
-      File compilerWrapper =
-          findOrCreateCompilerWrapperScript(
-              compilerWrapperCache,
-              toolchainIdeInfo,
-              blazeProjectData.workspacePathResolver,
-              targetKey);
-      if (compilerWrapper != null) {
-        BlazeResolveConfiguration config =
-            BlazeResolveConfiguration.createConfigurationForTarget(
-                project,
-                new ExecutionRootPathResolver(
-                    blazeProjectData.blazeRoots, blazeProjectData.workspacePathResolver),
-                blazeProjectData.workspacePathResolver,
-                headerRoots,
-                blazeProjectData.targetMap.get(targetKey),
-                toolchainIdeInfo,
-                compilerWrapper);
-        if (config != null) {
-          return new MapEntry(targetKey, config);
+    if (toolchainIdeInfo == null) {
+      return null;
+    }
+    BlazeCompilerSettings compilerSettings = compilerSettingsMap.get(toolchainIdeInfo);
+    if (compilerSettings == null) {
+      return null;
+    }
+    BlazeResolveConfiguration config =
+        BlazeResolveConfiguration.createConfigurationForTarget(
+            project,
+            new ExecutionRootPathResolver(
+                Blaze.getBuildSystem(project),
+                WorkspaceRoot.fromProject(project),
+                blazeProjectData.blazeInfo.getExecutionRoot(),
+                blazeProjectData.workspacePathResolver),
+            blazeProjectData.workspacePathResolver,
+            headerRoots,
+            blazeProjectData.targetMap.get(targetKey),
+            toolchainIdeInfo,
+            compilerSettings,
+            compilerInfoCache);
+    if (config == null) {
+      return null;
+    }
+    return new MapEntry(targetKey, config);
+  }
+
+  private static ImmutableMap<CToolchainIdeInfo, BlazeCompilerSettings> buildCompilerSettingsMap(
+      BlazeContext context,
+      Project project,
+      ImmutableMap<TargetKey, CToolchainIdeInfo> toolchainLookupMap,
+      WorkspacePathResolver workspacePathResolver) {
+    Set<CToolchainIdeInfo> toolchains =
+        toolchainLookupMap.values().stream().distinct().collect(Collectors.toSet());
+    List<ListenableFuture<Map.Entry<CToolchainIdeInfo, BlazeCompilerSettings>>>
+        compilerSettingsFutures = new ArrayList<>();
+    for (CToolchainIdeInfo toolchain : toolchains) {
+      compilerSettingsFutures.add(
+          submit(
+              () -> {
+                BlazeCompilerSettings settings =
+                    createBlazeCompilerSettings(project, toolchain, workspacePathResolver);
+                if (settings == null) {
+                  return null;
+                }
+                return new SimpleImmutableEntry<>(toolchain, settings);
+              }));
+    }
+    ImmutableMap.Builder<CToolchainIdeInfo, BlazeCompilerSettings> compilerSettingsMap =
+        ImmutableMap.builder();
+    try {
+      List<Map.Entry<CToolchainIdeInfo, BlazeCompilerSettings>> createdSettings =
+          Futures.allAsList(compilerSettingsFutures).get();
+      for (Map.Entry<CToolchainIdeInfo, BlazeCompilerSettings> createdSetting : createdSettings) {
+        if (createdSetting != null) {
+          compilerSettingsMap.put(createdSetting);
         }
       }
+    } catch (InterruptedException e) {
+      Thread.currentThread().interrupt();
+      context.setCancelled();
+    } catch (ExecutionException e) {
+      IssueOutput.error("Could not build C compiler settings map: " + e).submit(context);
+      LOG.error("Could not build C compiler settings map", e);
     }
-    return null;
+    return compilerSettingsMap.build();
   }
 
   @Nullable
-  private static File findOrCreateCompilerWrapperScript(
-      Map<CToolchainIdeInfo, File> compilerWrapperCache,
+  private static BlazeCompilerSettings createBlazeCompilerSettings(
+      Project project,
       CToolchainIdeInfo toolchainIdeInfo,
-      WorkspacePathResolver workspacePathResolver,
-      TargetKey targetKey) {
-    File compilerWrapper = compilerWrapperCache.get(toolchainIdeInfo);
+      WorkspacePathResolver workspacePathResolver) {
+    File compilerWrapper = getCompilerWrapper(toolchainIdeInfo, workspacePathResolver);
     if (compilerWrapper == null) {
-      File cppExecutable = toolchainIdeInfo.cppExecutable.getAbsoluteOrRelativeFile();
-      if (cppExecutable != null && !cppExecutable.isAbsolute()) {
-        cppExecutable = workspacePathResolver.resolveToFile(cppExecutable.getPath());
-      }
-      if (cppExecutable == null) {
-        String errorMessage =
-            String.format(
-                "Unable to find compiler executable: %s for rule %s",
-                toolchainIdeInfo.cppExecutable.toString(), targetKey);
-        LOG.warn(errorMessage);
-        compilerWrapper = null;
-      } else {
-        compilerWrapper = createCompilerExecutableWrapper(cppExecutable);
-        if (compilerWrapper != null) {
-          compilerWrapperCache.put(toolchainIdeInfo, compilerWrapper);
-        }
-      }
+      return null;
     }
-    return compilerWrapper;
+    ImmutableList.Builder<String> cFlagsBuilder = ImmutableList.builder();
+    cFlagsBuilder.addAll(toolchainIdeInfo.baseCompilerOptions);
+    cFlagsBuilder.addAll(toolchainIdeInfo.cCompilerOptions);
+    cFlagsBuilder.addAll(toolchainIdeInfo.unfilteredCompilerOptions);
+
+    ImmutableList.Builder<String> cppFlagsBuilder = ImmutableList.builder();
+    cppFlagsBuilder.addAll(toolchainIdeInfo.baseCompilerOptions);
+    cppFlagsBuilder.addAll(toolchainIdeInfo.cppCompilerOptions);
+    cppFlagsBuilder.addAll(toolchainIdeInfo.unfilteredCompilerOptions);
+    return new BlazeCompilerSettings(
+        project, compilerWrapper, compilerWrapper, cFlagsBuilder.build(), cppFlagsBuilder.build());
+  }
+
+  @Nullable
+  private static File getCompilerWrapper(
+      CToolchainIdeInfo toolchainIdeInfo, WorkspacePathResolver workspacePathResolver) {
+    File cppExecutable = toolchainIdeInfo.cppExecutable.getAbsoluteOrRelativeFile();
+    if (cppExecutable != null && !cppExecutable.isAbsolute()) {
+      cppExecutable = workspacePathResolver.resolveToFile(cppExecutable.getPath());
+    }
+    if (cppExecutable == null) {
+      LOG.warn(
+          String.format(
+              "Unable to find compiler executable: %s for toolchain %s",
+              toolchainIdeInfo.cppExecutable.toString(), toolchainIdeInfo));
+      return null;
+    }
+    return createCompilerExecutableWrapper(cppExecutable);
   }
 
   /**
@@ -352,24 +436,21 @@
   @Nullable
   public OCResolveConfiguration getConfigurationForFile(VirtualFile sourceFile) {
     SourceToTargetMap sourceToTargetMap = SourceToTargetMap.getInstance(project);
-    List<TargetKey> targetsForSourceFile =
-        Lists.newArrayList(
-            sourceToTargetMap.getRulesForSourceFile(VfsUtilCore.virtualToIoFile(sourceFile)));
+    ImmutableCollection<TargetKey> targetsForSourceFile =
+        sourceToTargetMap.getRulesForSourceFile(VfsUtilCore.virtualToIoFile(sourceFile));
     if (targetsForSourceFile.isEmpty()) {
       return null;
     }
 
-    // If a source file is in two different targets,
-    // we can't possibly show how it will be interpreted in both contexts at the same time
-    // in the IDE, so just pick the first target after we sort.
-    targetsForSourceFile.sort((o1, o2) -> o1.toString().compareTo(o2.toString()));
-    TargetKey targetKey = Iterables.getFirst(targetsForSourceFile, null);
+    // If a source file is in two different targets, we can't possibly show how it will be
+    // interpreted in both contexts at the same time in the IDE, so just pick the "first" target.
+    TargetKey targetKey = targetsForSourceFile.stream().min(TargetKey::compareTo).orElse(null);
     assert (targetKey != null);
 
     return resolveConfigurations.get(targetKey);
   }
 
-  public List<? extends OCResolveConfiguration> getAllConfigurations() {
-    return ImmutableList.copyOf(resolveConfigurations.values());
+  ImmutableList<? extends OCResolveConfiguration> getAllConfigurations() {
+    return resolveConfigurations.values().asList();
   }
 }
diff --git a/cpp/src/com/google/idea/blaze/cpp/BlazeCppSymbolRebuildSyncListener.java b/cpp/src/com/google/idea/blaze/cpp/BlazeCppSymbolRebuildSyncListener.java
index 07b1743..fb9e190 100644
--- a/cpp/src/com/google/idea/blaze/cpp/BlazeCppSymbolRebuildSyncListener.java
+++ b/cpp/src/com/google/idea/blaze/cpp/BlazeCppSymbolRebuildSyncListener.java
@@ -21,12 +21,12 @@
 import com.google.idea.blaze.base.settings.BlazeImportSettings;
 import com.google.idea.blaze.base.sync.BlazeSyncParams.SyncMode;
 import com.google.idea.blaze.base.sync.SyncListener;
+import com.google.idea.sdkcompat.cidr.OCWorkspaceModificationTrackersCompatUtils;
 import com.google.idea.sdkcompat.transactions.Transactions;
 import com.intellij.openapi.application.ApplicationManager;
 import com.intellij.openapi.project.Project;
 import com.jetbrains.cidr.lang.workspace.OCWorkspace;
 import com.jetbrains.cidr.lang.workspace.OCWorkspaceManager;
-import com.jetbrains.cidr.lang.workspace.OCWorkspaceModificationTrackers;
 
 /** Runs after sync, triggering a rebuild of the symbol tables. */
 public class BlazeCppSymbolRebuildSyncListener extends SyncListener.Adapter {
@@ -45,20 +45,16 @@
     if (!(workspace instanceof BlazeCWorkspace)) {
       return;
     }
-    rebuildSymbolTables((BlazeCWorkspace) workspace);
+    rebuildSymbolTables(project);
   }
 
-  private static void rebuildSymbolTables(BlazeCWorkspace workspace) {
-    OCWorkspaceModificationTrackers modTrackers = workspace.getModificationTrackers();
+  private static void rebuildSymbolTables(Project project) {
     Transactions.submitTransactionAndWait(
         () ->
             ApplicationManager.getApplication()
                 .runWriteAction(
-                    () -> {
-                      modTrackers.getProjectFilesListTracker().incModificationCount();
-                      modTrackers.getSourceFilesListTracker().incModificationCount();
-                      modTrackers.getBuildConfigurationChangesTracker().incModificationCount();
-                      modTrackers.getBuildSettingsChangesTracker().incModificationCount();
-                    }));
+                    () ->
+                        OCWorkspaceModificationTrackersCompatUtils.incrementModificationCounts(
+                            project)));
   }
 }
diff --git a/cpp/src/com/google/idea/blaze/cpp/BlazeResolveConfigurationTemporaryBase.java b/cpp/src/com/google/idea/blaze/cpp/BlazeResolveConfiguration.java
similarity index 66%
rename from cpp/src/com/google/idea/blaze/cpp/BlazeResolveConfigurationTemporaryBase.java
rename to cpp/src/com/google/idea/blaze/cpp/BlazeResolveConfiguration.java
index 2b04ff2..6581b12 100644
--- a/cpp/src/com/google/idea/blaze/cpp/BlazeResolveConfigurationTemporaryBase.java
+++ b/cpp/src/com/google/idea/blaze/cpp/BlazeResolveConfiguration.java
@@ -18,9 +18,8 @@
 import com.google.common.collect.ImmutableCollection;
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableMap;
-import com.google.common.collect.ImmutableMultimap;
 import com.google.common.collect.ImmutableSet;
-import com.google.common.collect.Lists;
+import com.google.common.collect.Iterables;
 import com.google.common.collect.Maps;
 import com.google.idea.blaze.base.ideinfo.CIdeInfo;
 import com.google.idea.blaze.base.ideinfo.CToolchainIdeInfo;
@@ -28,13 +27,18 @@
 import com.google.idea.blaze.base.ideinfo.TargetKey;
 import com.google.idea.blaze.base.ideinfo.TargetMap;
 import com.google.idea.blaze.base.model.primitives.ExecutionRootPath;
+import com.google.idea.blaze.base.model.primitives.Kind;
+import com.google.idea.blaze.base.model.primitives.LanguageClass;
 import com.google.idea.blaze.base.scope.BlazeContext;
 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.TimingScope;
 import com.google.idea.blaze.base.sync.workspace.ExecutionRootPathResolver;
 import com.google.idea.blaze.base.sync.workspace.WorkspacePathResolver;
+import com.google.idea.sdkcompat.cidr.OCResolveConfigurationAdapter;
 import com.intellij.openapi.diagnostic.Logger;
 import com.intellij.openapi.project.Project;
+import com.intellij.openapi.util.Pair;
 import com.intellij.openapi.util.UserDataHolderBase;
 import com.intellij.openapi.vfs.VirtualFile;
 import com.jetbrains.cidr.lang.OCFileTypeHelpers;
@@ -52,22 +56,20 @@
 import com.jetbrains.cidr.lang.workspace.headerRoots.IncludedHeadersRoot;
 import com.jetbrains.cidr.toolchains.CompilerInfoCache;
 import java.io.File;
-import java.util.ArrayList;
+import java.util.Collection;
 import java.util.List;
 import java.util.Map;
 import java.util.Objects;
+import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ConcurrentMap;
+import java.util.stream.Collectors;
 import javax.annotation.Nullable;
 
-/**
- * This is a temporary base class to deal with API changes between v145 (Android Studio) and v162
- * (CLion). Once Android Studio's API has caught up, the features in versioned/v162 can be merged,
- * this class be renamed BlazeResolveConfiguration, and it can be made final.
- */
-abstract class BlazeResolveConfigurationTemporaryBase extends UserDataHolderBase
-    implements OCResolveConfiguration {
+final class BlazeResolveConfiguration extends UserDataHolderBase
+    implements OCResolveConfigurationAdapter {
 
-  public static final Logger LOG = Logger.getInstance(BlazeResolveConfiguration.class);
-
+  private static final Logger logger = Logger.getInstance(BlazeResolveConfiguration.class);
   private final ExecutionRootPathResolver executionRootPathResolver;
   private final WorkspacePathResolver workspacePathResolver;
 
@@ -78,10 +80,13 @@
   private final ImmutableList<HeadersSearchRoot> cLibraryIncludeRoots;
   private final ImmutableList<HeadersSearchRoot> cppLibraryIncludeRoots;
   private final HeaderRoots projectIncludeRoots;
+  private final ConcurrentMap<Pair<OCLanguageKind, VirtualFile>, HeaderRoots> libraryIncludeRoots =
+      new ConcurrentHashMap<>();
 
   private final CompilerInfoCache compilerInfoCache;
   private final BlazeCompilerMacros compilerMacros;
   private final BlazeCompilerSettings compilerSettings;
+  private final CToolchainIdeInfo toolchainIdeInfo;
 
   @Nullable
   public static BlazeResolveConfiguration createConfigurationForTarget(
@@ -91,7 +96,8 @@
       ImmutableMap<File, VirtualFile> headerRoots,
       TargetIdeInfo target,
       CToolchainIdeInfo toolchainIdeInfo,
-      File compilerWrapper) {
+      BlazeCompilerSettings compilerSettings,
+      CompilerInfoCache compilerInfoCache) {
     CIdeInfo cIdeInfo = target.cIdeInfo;
     if (cIdeInfo == null) {
       return null;
@@ -104,19 +110,14 @@
 
     ImmutableSet.Builder<ExecutionRootPath> userIncludesBuilder = ImmutableSet.builder();
     userIncludesBuilder.addAll(cIdeInfo.transitiveIncludeDirectories);
+    userIncludesBuilder.addAll(cIdeInfo.localIncludeDirectories);
 
     ImmutableSet.Builder<ExecutionRootPath> userQuoteIncludesBuilder = ImmutableSet.builder();
     userQuoteIncludesBuilder.addAll(cIdeInfo.transitiveQuoteIncludeDirectories);
 
-    ImmutableList.Builder<String> cFlagsBuilder = ImmutableList.builder();
-    cFlagsBuilder.addAll(toolchainIdeInfo.baseCompilerOptions);
-    cFlagsBuilder.addAll(toolchainIdeInfo.cCompilerOptions);
-    cFlagsBuilder.addAll(toolchainIdeInfo.unfilteredCompilerOptions);
-
-    ImmutableList.Builder<String> cppFlagsBuilder = ImmutableList.builder();
-    cppFlagsBuilder.addAll(toolchainIdeInfo.baseCompilerOptions);
-    cppFlagsBuilder.addAll(toolchainIdeInfo.cppCompilerOptions);
-    cppFlagsBuilder.addAll(toolchainIdeInfo.unfilteredCompilerOptions);
+    ImmutableList.Builder<String> defines = ImmutableList.builder();
+    defines.addAll(cIdeInfo.transitiveDefines);
+    defines.addAll(cIdeInfo.localDefines);
 
     ImmutableMap<String, String> features = ImmutableMap.of();
 
@@ -131,53 +132,80 @@
         userQuoteIncludesBuilder.build(),
         userIncludesBuilder.build(),
         userIncludesBuilder.build(),
-        cIdeInfo.transitiveDefines,
+        defines.build(),
         features,
-        compilerWrapper,
-        compilerWrapper,
-        cFlagsBuilder.build(),
-        cppFlagsBuilder.build());
+        compilerSettings,
+        compilerInfoCache,
+        toolchainIdeInfo);
   }
 
-  public static ImmutableMap<TargetKey, CToolchainIdeInfo> buildToolchainLookupMap(
-      BlazeContext context,
-      TargetMap targetMap,
-      ImmutableMultimap<TargetKey, TargetKey> reverseDependencies) {
+  static ImmutableMap<TargetKey, CToolchainIdeInfo> buildToolchainLookupMap(
+      BlazeContext context, TargetMap targetMap) {
     return Scope.push(
         context,
         childContext -> {
           childContext.push(new TimingScope("Build toolchain lookup map"));
 
-          List<TargetKey> seeds = Lists.newArrayList();
+          Map<TargetKey, CToolchainIdeInfo> toolchains = Maps.newLinkedHashMap();
           for (TargetIdeInfo target : targetMap.targets()) {
             CToolchainIdeInfo cToolchainIdeInfo = target.cToolchainIdeInfo;
             if (cToolchainIdeInfo != null) {
-              seeds.add(target.key);
+              toolchains.put(target.key, cToolchainIdeInfo);
             }
           }
 
-          Map<TargetKey, CToolchainIdeInfo> lookupTable = Maps.newHashMap();
-          for (TargetKey seed : seeds) {
-            CToolchainIdeInfo toolchainInfo = targetMap.get(seed).cToolchainIdeInfo;
-            LOG.assertTrue(toolchainInfo != null);
-            List<TargetKey> worklist = Lists.newArrayList(reverseDependencies.get(seed));
-            while (!worklist.isEmpty()) {
-              // We should never see a label depend on two different toolchains.
-              TargetKey l = worklist.remove(0);
-              CToolchainIdeInfo previousValue = lookupTable.putIfAbsent(l, toolchainInfo);
-              // Don't propagate the toolchain twice.
-              if (previousValue == null) {
-                worklist.addAll(reverseDependencies.get(l));
-              } else {
-                LOG.assertTrue(previousValue.equals(toolchainInfo));
+          ImmutableMap.Builder<TargetKey, CToolchainIdeInfo> lookupTable = ImmutableMap.builder();
+          for (TargetIdeInfo target : targetMap.targets()) {
+            if (target.kind.getLanguageClass() != LanguageClass.C
+                || target.kind == Kind.CC_TOOLCHAIN) {
+              continue;
+            }
+            List<TargetKey> toolchainDeps =
+                target
+                    .dependencies
+                    .stream()
+                    .map(dep -> dep.targetKey)
+                    .filter(toolchains::containsKey)
+                    .collect(Collectors.toList());
+            if (toolchainDeps.size() != 1) {
+              issueToolchainWarning(context, target, toolchainDeps);
+            }
+            if (!toolchainDeps.isEmpty()) {
+              TargetKey toolchainKey = toolchainDeps.get(0);
+              CToolchainIdeInfo toolchainInfo = toolchains.get(toolchainKey);
+              lookupTable.put(target.key, toolchainInfo);
+            } else {
+              CToolchainIdeInfo arbitraryToolchain = Iterables.getFirst(toolchains.values(), null);
+              if (arbitraryToolchain != null) {
+                lookupTable.put(target.key, arbitraryToolchain);
               }
             }
           }
-          return ImmutableMap.copyOf(lookupTable);
+          return lookupTable.build();
         });
   }
 
-  public BlazeResolveConfigurationTemporaryBase(
+  private static void issueToolchainWarning(
+      BlazeContext context, TargetIdeInfo target, List<TargetKey> toolchainDeps) {
+    String warningMessage =
+        String.format(
+            "cc target %s does not depend on exactly 1 cc toolchain. " + " Found %d toolchains.",
+            target.key, toolchainDeps.size());
+    if (usesAppleCcToolchain(target)) {
+      logger.warn(warningMessage + " (apple_cc_toolchain)");
+    } else {
+      IssueOutput.warn(warningMessage).submit(context);
+    }
+  }
+
+  private static boolean usesAppleCcToolchain(TargetIdeInfo target) {
+    return target
+        .dependencies
+        .stream()
+        .anyMatch(dep -> dep.targetKey.label.toString().startsWith("//tools/osx/crosstool"));
+  }
+
+  public BlazeResolveConfiguration(
       Project project,
       ExecutionRootPathResolver executionRootPathResolver,
       WorkspacePathResolver workspacePathResolver,
@@ -190,14 +218,14 @@
       ImmutableCollection<ExecutionRootPath> cppIncludeDirs,
       ImmutableCollection<String> defines,
       ImmutableMap<String, String> features,
-      File cCompilerExecutable,
-      File cppCompilerExecutable,
-      ImmutableList<String> cCompilerFlags,
-      ImmutableList<String> cppCompilerFlags) {
+      BlazeCompilerSettings compilerSettings,
+      CompilerInfoCache compilerInfoCache,
+      CToolchainIdeInfo toolchainIdeInfo) {
     this.executionRootPathResolver = executionRootPathResolver;
     this.workspacePathResolver = workspacePathResolver;
     this.project = project;
     this.targetKey = targetKey;
+    this.toolchainIdeInfo = toolchainIdeInfo;
 
     ImmutableList.Builder<HeadersSearchRoot> cIncludeRootsBuilder = ImmutableList.builder();
     collectHeaderRoots(headerRoots, cIncludeRootsBuilder, cIncludeDirs, true /* isUserHeader */);
@@ -217,11 +245,8 @@
         headerRoots, quoteIncludeRootsBuilder, quoteIncludeDirs, true /* isUserHeader */);
     this.projectIncludeRoots = new HeaderRoots(quoteIncludeRootsBuilder.build());
 
-    this.compilerSettings =
-        new BlazeCompilerSettings(
-            project, cCompilerExecutable, cppCompilerExecutable, cCompilerFlags, cppCompilerFlags);
-
-    this.compilerInfoCache = new CompilerInfoCache();
+    this.compilerSettings = compilerSettings;
+    this.compilerInfoCache = compilerInfoCache;
     this.compilerMacros =
         new BlazeCompilerMacros(project, compilerInfoCache, compilerSettings, defines, features);
   }
@@ -242,12 +267,6 @@
 
   @Nullable
   @Override
-  public VirtualFile getPrecompiledHeader() {
-    return null;
-  }
-
-  @Nullable
-  @Override
   public OCLanguageKind getDeclaredLanguageKind(VirtualFile sourceOrHeaderFile) {
     String fileName = sourceOrHeaderFile.getName();
     if (OCFileTypeHelpers.isSourceFile(fileName)) {
@@ -268,8 +287,7 @@
 
   @Nullable
   private VirtualFile getSourceFileForHeaderFile(VirtualFile headerFile) {
-    ArrayList<VirtualFile> roots =
-        new ArrayList<>(OCImportGraph.getAllHeaderRoots(project, headerFile));
+    Collection<VirtualFile> roots = OCImportGraph.getAllHeaderRoots(project, headerFile);
 
     final String headerNameWithoutExtension = headerFile.getNameWithoutExtension();
     for (VirtualFile root : roots) {
@@ -281,11 +299,6 @@
   }
 
   @Override
-  public OCLanguageKind getPrecompiledLanguageKind() {
-    return getMaximumLanguageKind();
-  }
-
-  @Override
   public OCLanguageKind getMaximumLanguageKind() {
     return OCLanguageKind.CPP;
   }
@@ -302,21 +315,27 @@
     if (languageKind == null) {
       languageKind = getLanguageKind(sourceFile);
     }
+    Pair<OCLanguageKind, VirtualFile> cacheKey = Pair.create(languageKind, sourceFile);
+    return libraryIncludeRoots.computeIfAbsent(
+        cacheKey,
+        key -> {
+          OCLanguageKind lang = key.first;
+          VirtualFile source = key.second;
+          ImmutableSet.Builder<HeadersSearchRoot> roots = ImmutableSet.builder();
+          if (lang == OCLanguageKind.C) {
+            roots.addAll(cLibraryIncludeRoots);
+          } else {
+            roots.addAll(cppLibraryIncludeRoots);
+          }
 
-    ImmutableSet.Builder<HeadersSearchRoot> roots = ImmutableSet.builder();
-    if (languageKind == OCLanguageKind.C) {
-      roots.addAll(cLibraryIncludeRoots);
-    } else {
-      roots.addAll(cppLibraryIncludeRoots);
-    }
-
-    CidrCompilerResult<CompilerInfoCache.Entry> compilerInfoCacheHolder =
-        compilerInfoCache.getCompilerInfoCache(project, compilerSettings, languageKind, sourceFile);
-    CompilerInfoCache.Entry compilerInfo = compilerInfoCacheHolder.getResult();
-    if (compilerInfo != null) {
-      roots.addAll(compilerInfo.headerSearchPaths);
-    }
-    return new HeaderRoots(roots.build().asList());
+          CidrCompilerResult<CompilerInfoCache.Entry> compilerInfoCacheHolder =
+              compilerInfoCache.getCompilerInfoCache(project, compilerSettings, lang, source);
+          CompilerInfoCache.Entry compilerInfo = compilerInfoCacheHolder.getResult();
+          if (compilerInfo != null) {
+            roots.addAll(compilerInfo.headerSearchPaths);
+          }
+          return new HeaderRoots(roots.build().asList());
+        });
   }
 
   private void collectHeaderRoots(
@@ -346,10 +365,9 @@
     return compilerSettings;
   }
 
-  @Nullable
   @Override
   public Object getIndexingCluster() {
-    return null;
+    return toolchainIdeInfo;
   }
 
   @Override
@@ -376,4 +394,35 @@
     BlazeResolveConfiguration that = (BlazeResolveConfiguration) obj;
     return compareTo(that) == 0;
   }
+
+  /* This function is part of the v162/v163 plugin APIs. */
+  @Nullable
+  @Override
+  public VirtualFile getPrecompiledHeader() {
+    return null;
+  }
+
+  /* This function is part of the v162/v163 plugin APIs. */
+  @Override
+  public OCLanguageKind getPrecompiledLanguageKind() {
+    return getMaximumLanguageKind();
+  }
+
+  /* This function is part of the v171 plugin API. */
+  @Override
+  public Set<VirtualFile> getPrecompiledHeaders() {
+    return ImmutableSet.of();
+  }
+
+  /* This function is part of the v171 plugin API. */
+  @Override
+  public List<VirtualFile> getPrecompiledHeaders(OCLanguageKind kind, VirtualFile sourceFile) {
+    return ImmutableList.of();
+  }
+
+  /* This function is part of the v171 plugin API. */
+  @Override
+  public Collection<VirtualFile> getSources() {
+    return ImmutableList.of();
+  }
 }
diff --git a/cpp/src/com/google/idea/blaze/cpp/CPrefetchFileSource.java b/cpp/src/com/google/idea/blaze/cpp/CPrefetchFileSource.java
index ff0b0ed..4e63e71 100644
--- a/cpp/src/com/google/idea/blaze/cpp/CPrefetchFileSource.java
+++ b/cpp/src/com/google/idea/blaze/cpp/CPrefetchFileSource.java
@@ -18,6 +18,7 @@
 import com.google.common.collect.ImmutableSet;
 import com.google.idea.blaze.base.model.BlazeProjectData;
 import com.google.idea.blaze.base.prefetch.PrefetchFileSource;
+import com.google.idea.blaze.base.projectview.ProjectViewSet;
 import com.intellij.openapi.project.Project;
 import java.io.File;
 import java.util.Collection;
@@ -27,7 +28,10 @@
 public class CPrefetchFileSource implements PrefetchFileSource {
   @Override
   public void addFilesToPrefetch(
-      Project project, BlazeProjectData blazeProjectData, Collection<File> files) {}
+      Project project,
+      ProjectViewSet projectViewSet,
+      BlazeProjectData blazeProjectData,
+      Collection<File> files) {}
 
   @Override
   public Set<String> prefetchSrcFileExtensions() {
diff --git a/cpp/src/com/google/idea/blaze/cpp/CidrSymbolBuilderSuppressor.java b/cpp/src/com/google/idea/blaze/cpp/CidrSymbolBuilderSuppressor.java
new file mode 100644
index 0000000..1abc384
--- /dev/null
+++ b/cpp/src/com/google/idea/blaze/cpp/CidrSymbolBuilderSuppressor.java
@@ -0,0 +1,82 @@
+/*
+ * Copyright 2017 The Bazel Authors. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.idea.blaze.cpp;
+
+import com.google.common.base.Preconditions;
+import com.google.common.collect.ImmutableList;
+import com.google.idea.blaze.base.settings.Blaze;
+import com.intellij.openapi.components.ApplicationComponent;
+import com.intellij.openapi.extensions.ExtensionPoint;
+import com.intellij.openapi.extensions.Extensions;
+import com.intellij.openapi.extensions.LoadingOrder;
+import com.intellij.openapi.project.Project;
+import com.intellij.openapi.startup.StartupActivity;
+
+/**
+ * cidr-lang usually registers some StartupActivity instances that get run on project open to
+ * rebuild symbols. This causes problems with blaze projects because this happens before the project
+ * configuration has been set up. CLwB/ASwB trigger a symbol rebuild after the startup sync.
+ */
+public class CidrSymbolBuilderSuppressor implements ApplicationComponent {
+  private static final ImmutableList<Class<? extends StartupActivity>>
+      STARTUP_ACTIVITIES_TO_SUPPRESS =
+          ImmutableList.of(
+              com.jetbrains.cidr.lang.symbols.symtable.OCInitialTablesBuildingActivity.class,
+              com.jetbrains.cidr.modulemap.resolve.ModuleMapInitialBuildingActivity.class);
+
+  private void addFiltersToStartupActivities() {
+    ExtensionPoint<StartupActivity> ep =
+        Extensions.getRootArea().getExtensionPoint(StartupActivity.POST_STARTUP_ACTIVITY);
+    for (Class<? extends StartupActivity> startupActivity : STARTUP_ACTIVITIES_TO_SUPPRESS) {
+      StartupActivity startupActivityInstance =
+          StartupActivity.POST_STARTUP_ACTIVITY.findExtension(startupActivity);
+      Preconditions.checkNotNull(startupActivityInstance);
+      StartupActivity replacementStartupActivity =
+          new BlazeSuppressStartupActivity(startupActivityInstance);
+      ep.registerExtension(
+          replacementStartupActivity, LoadingOrder.before(startupActivity.getSimpleName()));
+      ep.unregisterExtension(startupActivityInstance);
+    }
+  }
+
+  @Override
+  public void initComponent() {
+    addFiltersToStartupActivities();
+  }
+
+  @Override
+  public void disposeComponent() {}
+
+  @Override
+  public String getComponentName() {
+    return "CidrSymbolBuilderSuppressor";
+  }
+
+  private static class BlazeSuppressStartupActivity implements StartupActivity {
+    final StartupActivity original;
+
+    private BlazeSuppressStartupActivity(StartupActivity original) {
+      this.original = original;
+    }
+
+    @Override
+    public void runActivity(Project project) {
+      if (!Blaze.isBlazeProject(project)) {
+        original.runActivity(project);
+      }
+    }
+  }
+}
diff --git a/cpp/src/com/google/idea/blaze/cpp/versioned/v145/BlazeResolveConfiguration.java b/cpp/src/com/google/idea/blaze/cpp/versioned/v145/BlazeResolveConfiguration.java
deleted file mode 100644
index b2d63a0..0000000
--- a/cpp/src/com/google/idea/blaze/cpp/versioned/v145/BlazeResolveConfiguration.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.cpp;
-
-import com.google.common.collect.ImmutableCollection;
-import com.google.common.collect.ImmutableList;
-import com.google.common.collect.ImmutableMap;
-import com.google.idea.blaze.base.ideinfo.TargetKey;
-import com.google.idea.blaze.base.model.primitives.ExecutionRootPath;
-import com.google.idea.blaze.base.sync.workspace.ExecutionRootPathResolver;
-import com.google.idea.blaze.base.sync.workspace.WorkspacePathResolver;
-import com.intellij.openapi.project.Project;
-import com.intellij.openapi.vfs.VirtualFile;
-import com.jetbrains.cidr.execution.CidrBuildTarget;
-import com.jetbrains.cidr.execution.CidrBuildTargetWithConfigurations;
-import com.jetbrains.cidr.execution.CidrTargetHolder;
-import java.io.File;
-import java.util.List;
-import javax.annotation.Nullable;
-import javax.swing.Icon;
-
-final class BlazeResolveConfiguration extends BlazeResolveConfigurationTemporaryBase
-    implements CidrTargetHolder {
-
-  public BlazeResolveConfiguration(
-      Project project,
-      ExecutionRootPathResolver executionRootPathResolver,
-      WorkspacePathResolver workspacePathResolver,
-      ImmutableMap<File, VirtualFile> headerRoots,
-      TargetKey targetKey,
-      ImmutableCollection<ExecutionRootPath> cSystemIncludeDirs,
-      ImmutableCollection<ExecutionRootPath> cppSystemIncludeDirs,
-      ImmutableCollection<ExecutionRootPath> quoteIncludeDirs,
-      ImmutableCollection<ExecutionRootPath> cIncludeDirs,
-      ImmutableCollection<ExecutionRootPath> cppIncludeDirs,
-      ImmutableCollection<String> defines,
-      ImmutableMap<String, String> features,
-      File cCompilerExecutable,
-      File cppCompilerExecutable,
-      ImmutableList<String> cCompilerFlags,
-      ImmutableList<String> cppCompilerFlags) {
-    super(
-        project,
-        executionRootPathResolver,
-        workspacePathResolver,
-        headerRoots,
-        targetKey,
-        cSystemIncludeDirs,
-        cppSystemIncludeDirs,
-        quoteIncludeDirs,
-        cIncludeDirs,
-        cppIncludeDirs,
-        defines,
-        features,
-        cCompilerExecutable,
-        cppCompilerExecutable,
-        cCompilerFlags,
-        cppCompilerFlags);
-  }
-
-  /** Workaround for b/30301958. TODO: Remove this once we move to CLion 162.1531.1 or later */
-  @Override
-  public CidrBuildTarget getTarget() {
-    return new CidrBuildTargetWithConfigurations() {
-      @Override
-      public String getName() {
-        return targetKey.toString();
-      }
-
-      @Override
-      public String getProjectName() {
-        return project.getName();
-      }
-
-      @Nullable
-      @Override
-      public Icon getIcon() {
-        return null;
-      }
-
-      @Override
-      public boolean isExecutable() {
-        return false;
-      }
-
-      @Override
-      public List getBuildConfigurations() {
-        return ImmutableList.of();
-      }
-    };
-  }
-}
diff --git a/cpp/src/com/google/idea/blaze/cpp/versioned/v162/BlazeResolveConfiguration.java b/cpp/src/com/google/idea/blaze/cpp/versioned/v162/BlazeResolveConfiguration.java
deleted file mode 100644
index 959936b..0000000
--- a/cpp/src/com/google/idea/blaze/cpp/versioned/v162/BlazeResolveConfiguration.java
+++ /dev/null
@@ -1,66 +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.cpp;
-
-import com.google.common.collect.ImmutableCollection;
-import com.google.common.collect.ImmutableList;
-import com.google.common.collect.ImmutableMap;
-import com.google.idea.blaze.base.ideinfo.TargetKey;
-import com.google.idea.blaze.base.model.primitives.ExecutionRootPath;
-import com.google.idea.blaze.base.sync.workspace.ExecutionRootPathResolver;
-import com.google.idea.blaze.base.sync.workspace.WorkspacePathResolver;
-import com.intellij.openapi.project.Project;
-import com.intellij.openapi.vfs.VirtualFile;
-import java.io.File;
-
-final class BlazeResolveConfiguration extends BlazeResolveConfigurationTemporaryBase {
-
-  public BlazeResolveConfiguration(
-      Project project,
-      ExecutionRootPathResolver executionRootPathResolver,
-      WorkspacePathResolver workspacePathResolver,
-      ImmutableMap<File, VirtualFile> headerRoots,
-      TargetKey targetKey,
-      ImmutableCollection<ExecutionRootPath> cSystemIncludeDirs,
-      ImmutableCollection<ExecutionRootPath> cppSystemIncludeDirs,
-      ImmutableCollection<ExecutionRootPath> quoteIncludeDirs,
-      ImmutableCollection<ExecutionRootPath> cIncludeDirs,
-      ImmutableCollection<ExecutionRootPath> cppIncludeDirs,
-      ImmutableCollection<String> defines,
-      ImmutableMap<String, String> features,
-      File cCompilerExecutable,
-      File cppCompilerExecutable,
-      ImmutableList<String> cCompilerFlags,
-      ImmutableList<String> cppCompilerFlags) {
-    super(
-        project,
-        executionRootPathResolver,
-        workspacePathResolver,
-        headerRoots,
-        targetKey,
-        cSystemIncludeDirs,
-        cppSystemIncludeDirs,
-        quoteIncludeDirs,
-        cIncludeDirs,
-        cppIncludeDirs,
-        defines,
-        features,
-        cCompilerExecutable,
-        cppCompilerExecutable,
-        cCompilerFlags,
-        cppCompilerFlags);
-  }
-}
diff --git a/cpp/tests/unittests/com/google/idea/blaze/cpp/BlazeCompilerSettingsTest.java b/cpp/tests/unittests/com/google/idea/blaze/cpp/BlazeCompilerSettingsTest.java
index 317d838..a1e748c 100644
--- a/cpp/tests/unittests/com/google/idea/blaze/cpp/BlazeCompilerSettingsTest.java
+++ b/cpp/tests/unittests/com/google/idea/blaze/cpp/BlazeCompilerSettingsTest.java
@@ -19,6 +19,7 @@
 
 import com.google.common.collect.ImmutableList;
 import com.google.idea.blaze.base.BlazeTestCase;
+import com.google.idea.sdkcompat.cidr.CidrCompilerSwitchesAdapter;
 import com.jetbrains.cidr.lang.OCLanguageKind;
 import com.jetbrains.cidr.lang.toolchains.CidrCompilerSwitches;
 import java.io.File;
@@ -39,43 +40,7 @@
         new BlazeCompilerSettings(getProject(), cppExe, cppExe, cFlags, cFlags);
 
     CidrCompilerSwitches compilerSwitches = settings.getCompilerSwitches(OCLanguageKind.C, null);
-    List<String> commandLineArgs = compilerSwitches.getFileArgs();
+    List<String> commandLineArgs = CidrCompilerSwitchesAdapter.getFileArgs(compilerSwitches);
     assertThat(commandLineArgs).containsExactly("-fast", "-slow");
   }
-
-  @Test
-  public void testCompilerSwitchesWithUnescapedSpaces() {
-    File cppExe = new File("bin/cpp");
-    ImmutableList<String> cFlags = ImmutableList.of("-f ast", "-slo w");
-    BlazeCompilerSettings settings =
-        new BlazeCompilerSettings(getProject(), cppExe, cppExe, cFlags, cFlags);
-
-    CidrCompilerSwitches compilerSwitches = settings.getCompilerSwitches(OCLanguageKind.C, null);
-    List<String> fileArgs = compilerSwitches.getFileArgs();
-    assertThat(fileArgs).containsExactly("-f", "ast", "-slo", "w");
-  }
-
-  @Test
-  public void testCompilerSwitchesWithEscapedSpaces() {
-    File cppExe = new File("bin/cpp");
-    ImmutableList<String> cFlags = ImmutableList.of("-f\\ ast", "-slo\\ w");
-    BlazeCompilerSettings settings =
-        new BlazeCompilerSettings(getProject(), cppExe, cppExe, cFlags, cFlags);
-
-    CidrCompilerSwitches compilerSwitches = settings.getCompilerSwitches(OCLanguageKind.C, null);
-    List<String> fileArgs = compilerSwitches.getFileArgs();
-    assertThat(fileArgs).containsExactly("-f", "ast", "-slo", "w");
-  }
-
-  @Test
-  public void testCompilerSwitchesWithUnescapedAndEscapedSpaces() {
-    File cppExe = new File("bin/cpp");
-    ImmutableList<String> cFlags = ImmutableList.of("-f ast", "-slo\\ w");
-    BlazeCompilerSettings settings =
-        new BlazeCompilerSettings(getProject(), cppExe, cppExe, cFlags, cFlags);
-
-    CidrCompilerSwitches compilerSwitches = settings.getCompilerSwitches(OCLanguageKind.C, null);
-    List<String> fileArgs = compilerSwitches.getFileArgs();
-    assertThat(fileArgs).containsExactly("-f", "ast", "-slo", "w");
-  }
 }
diff --git a/golang/src/com/google/idea/blaze/golang/sync/BlazeGoSyncPlugin.java b/golang/src/com/google/idea/blaze/golang/sync/BlazeGoSyncPlugin.java
index 7f2a5c7..d65c8e6 100644
--- a/golang/src/com/google/idea/blaze/golang/sync/BlazeGoSyncPlugin.java
+++ b/golang/src/com/google/idea/blaze/golang/sync/BlazeGoSyncPlugin.java
@@ -23,6 +23,7 @@
 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.plugin.PluginUtils;
 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;
@@ -34,10 +35,7 @@
 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;
@@ -51,10 +49,7 @@
 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;
@@ -155,7 +150,8 @@
 
   @Nullable
   @Override
-  public LibrarySource getLibrarySource(BlazeProjectData blazeProjectData) {
+  public LibrarySource getLibrarySource(
+      ProjectViewSet projectViewSet, BlazeProjectData blazeProjectData) {
     if (!blazeProjectData.workspaceLanguageSettings.isLanguageActive(LanguageClass.GO)) {
       return null;
     }
@@ -164,41 +160,22 @@
 
   @Override
   public boolean validateProjectView(
+      @Nullable Project project,
       BlazeContext context,
       ProjectViewSet projectViewSet,
       WorkspaceLanguageSettings workspaceLanguageSettings) {
     if (!workspaceLanguageSettings.isLanguageActive(LanguageClass.GO)) {
       return true;
     }
-    if (!isPluginEnabled()) {
+    if (!PluginUtils.isPluginEnabled(GO_PLUGIN_ID)) {
       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);
-                  }
-                }
-              })
+          .navigatable(PluginUtils.installOrEnablePluginNavigable(GO_PLUGIN_ID))
           .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,
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
index e563a27..fe0cf37 100644
--- a/golang/tests/unittests/com/google/idea/blaze/golang/sync/BlazeGoSyncPluginTest.java
+++ b/golang/tests/unittests/com/google/idea/blaze/golang/sync/BlazeGoSyncPluginTest.java
@@ -71,7 +71,7 @@
                     .build())
             .build();
     WorkspaceLanguageSettings workspaceLanguageSettings =
-        LanguageSupport.createWorkspaceLanguageSettings(context, projectViewSet);
+        LanguageSupport.createWorkspaceLanguageSettings(projectViewSet);
     errorCollector.assertNoIssues();
     assertThat(workspaceLanguageSettings)
         .isEqualTo(
@@ -109,7 +109,9 @@
                     .add(ListSection.builder(AdditionalLanguagesSection.KEY).add(LanguageClass.GO))
                     .build())
             .build();
-    LanguageSupport.createWorkspaceLanguageSettings(context, projectViewSet);
+    WorkspaceLanguageSettings workspaceLanguageSettings =
+        LanguageSupport.createWorkspaceLanguageSettings(projectViewSet);
+    LanguageSupport.validateLanguageSettings(context, workspaceLanguageSettings);
     errorCollector.assertIssueContaining(
         "Language 'go' is not supported for this plugin with workspace type: 'java'");
   }
diff --git a/ijwb/BUILD b/ijwb/BUILD
index 55b2b72..ea9be4f 100644
--- a/ijwb/BUILD
+++ b/ijwb/BUILD
@@ -20,8 +20,9 @@
         "//golang:plugin_xml",
         "//java:plugin_xml",
         "//plugin_dev:plugin_xml",
+        "//python:plugin_xml",
+        "//scala:plugin_xml",
     ],
-    visibility = ["//visibility:public"],
 )
 
 merged_plugin_xml(
@@ -34,6 +35,7 @@
 
 stamped_plugin_xml(
     name = "stamped_plugin_xml",
+    changelog_file = "//:changelog",
     include_product_code_in_stamp = True,
     plugin_id = "com.google.idea.bazel.ijwb",
     plugin_name = "IntelliJ with Bazel",
@@ -45,21 +47,33 @@
 java_library(
     name = "ijwb_lib",
     srcs = glob(["src/**/*.java"]),
-    visibility = ["//visibility:public"],
     exports = [
         "//plugin_dev",
     ],
+    runtime_deps = [
+        "//golang",
+        "//python",
+        "//terminal",
+    ],
     deps = [
         "//base",
-        "//golang",
         "//intellij_platform_sdk:plugin_api",
         "//java",
+        "//scala",
+        "//sdkcompat",
         "@jsr305_annotations//jar",
     ],
 )
 
+OPTIONAL_PLUGIN_XMLS = [
+    "//scala:optional_xml",
+    "//python:optional_xml",
+    "//terminal:optional_xml",
+]
+
 intellij_plugin(
     name = "ijwb_bazel",
+    optional_plugin_xmls = OPTIONAL_PLUGIN_XMLS,
     plugin_xml = ":stamped_plugin_xml",
     deps = [
         ":ijwb_lib",
diff --git a/ijwb/src/META-INF/ijwb.xml b/ijwb/src/META-INF/ijwb.xml
index e4dc577..805e46e 100644
--- a/ijwb/src/META-INF/ijwb.xml
+++ b/ijwb/src/META-INF/ijwb.xml
@@ -26,7 +26,7 @@
     <SyncPlugin implementation="com.google.idea.blaze.ijwb.javascript.BlazeJavascriptSyncPlugin"/>
     <SyncPlugin implementation="com.google.idea.blaze.ijwb.typescript.BlazeTypescriptSyncPlugin"/>
     <SyncPlugin implementation="com.google.idea.blaze.ijwb.dart.BlazeDartSyncPlugin"/>
-    <java.JavaSyncAugmenter implementation="com.google.idea.blaze.ijwb.android.BlazeAndroidLiteJavaSyncAugmenter"/>
+    <JavaSyncAugmenter implementation="com.google.idea.blaze.ijwb.android.BlazeAndroidLiteJavaSyncAugmenter"/>
   </extensions>
 
 </idea-plugin>
diff --git a/ijwb/src/com/google/idea/blaze/ijwb/android/BlazeAndroidLiteJavaSyncAugmenter.java b/ijwb/src/com/google/idea/blaze/ijwb/android/BlazeAndroidLiteJavaSyncAugmenter.java
index 69ec1d8..c7a5e99 100644
--- a/ijwb/src/com/google/idea/blaze/ijwb/android/BlazeAndroidLiteJavaSyncAugmenter.java
+++ b/ijwb/src/com/google/idea/blaze/ijwb/android/BlazeAndroidLiteJavaSyncAugmenter.java
@@ -47,12 +47,12 @@
     // Add R.java jars
     LibraryArtifact resourceJar = androidIdeInfo.resourceJar;
     if (resourceJar != null) {
-      jars.add(new BlazeJarLibrary(resourceJar, target.key));
+      jars.add(new BlazeJarLibrary(resourceJar));
     }
 
     LibraryArtifact idlJar = androidIdeInfo.idlJar;
     if (idlJar != null) {
-      genJars.add(new BlazeJarLibrary(idlJar, target.key));
+      genJars.add(new BlazeJarLibrary(idlJar));
     }
   }
 }
diff --git a/ijwb/src/com/google/idea/blaze/ijwb/android/BlazeAndroidLiteSyncPlugin.java b/ijwb/src/com/google/idea/blaze/ijwb/android/BlazeAndroidLiteSyncPlugin.java
index 4a9612d..5f3e545 100644
--- a/ijwb/src/com/google/idea/blaze/ijwb/android/BlazeAndroidLiteSyncPlugin.java
+++ b/ijwb/src/com/google/idea/blaze/ijwb/android/BlazeAndroidLiteSyncPlugin.java
@@ -15,11 +15,26 @@
  */
 package com.google.idea.blaze.ijwb.android;
 
+import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableSet;
+import com.google.idea.blaze.base.ideinfo.AndroidSdkIdeInfo;
+import com.google.idea.blaze.base.ideinfo.ArtifactLocation;
+import com.google.idea.blaze.base.ideinfo.LibraryArtifact;
+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.primitives.LanguageClass;
 import com.google.idea.blaze.base.model.primitives.WorkspaceType;
+import com.google.idea.blaze.base.projectview.ProjectViewSet;
 import com.google.idea.blaze.base.sync.BlazeSyncPlugin;
+import com.google.idea.blaze.base.sync.libraries.LibrarySource;
+import com.google.idea.blaze.java.sync.model.BlazeJarLibrary;
+import java.util.Collection;
+import java.util.List;
+import java.util.Objects;
 import java.util.Set;
+import java.util.stream.Collectors;
+import javax.annotation.Nullable;
 
 /** Rudimentary support for android in IntelliJ. */
 public class BlazeAndroidLiteSyncPlugin extends BlazeSyncPlugin.Adapter {
@@ -33,4 +48,50 @@
         return ImmutableSet.of();
     }
   }
+
+  @Nullable
+  @Override
+  public LibrarySource getLibrarySource(
+      ProjectViewSet projectViewSet, BlazeProjectData blazeProjectData) {
+    if (!blazeProjectData.workspaceLanguageSettings.isLanguageActive(LanguageClass.ANDROID)) {
+      return null;
+    }
+    BlazeLibrary sdkLibrary = getSdkLibrary(blazeProjectData);
+    if (sdkLibrary == null) {
+      return null;
+    }
+    return new LibrarySource.Adapter() {
+      @Override
+      public Collection<? extends BlazeLibrary> getLibraries() {
+        return ImmutableList.of(sdkLibrary);
+      }
+    };
+  }
+
+  @Nullable
+  private static BlazeLibrary getSdkLibrary(BlazeProjectData blazeProjectData) {
+    List<AndroidSdkIdeInfo> sdkTargets = androidSdkTargets(blazeProjectData.targetMap);
+    if (sdkTargets.isEmpty()) {
+      return null;
+    }
+    // for now, just add the first one found
+    // TODO: warn if there's more than one
+    ArtifactLocation sdk =
+        sdkTargets
+            .stream()
+            .map(info -> info.androidJar)
+            .filter(Objects::nonNull)
+            .findFirst()
+            .orElse(null);
+    return sdk != null ? new BlazeJarLibrary(new LibraryArtifact(null, sdk, null)) : null;
+  }
+
+  private static List<AndroidSdkIdeInfo> androidSdkTargets(TargetMap targetMap) {
+    return targetMap
+        .targets()
+        .stream()
+        .map(target -> target.androidSdkIdeInfo)
+        .filter(Objects::nonNull)
+        .collect(Collectors.toList());
+  }
 }
diff --git a/ijwb/src/com/google/idea/blaze/ijwb/dart/BlazeDartLibrarySource.java b/ijwb/src/com/google/idea/blaze/ijwb/dart/BlazeDartLibrarySource.java
index 8772ee3..0bfbbf0 100644
--- a/ijwb/src/com/google/idea/blaze/ijwb/dart/BlazeDartLibrarySource.java
+++ b/ijwb/src/com/google/idea/blaze/ijwb/dart/BlazeDartLibrarySource.java
@@ -16,6 +16,7 @@
 package com.google.idea.blaze.ijwb.dart;
 
 import com.google.idea.blaze.base.sync.libraries.LibrarySource;
+import com.google.idea.sdkcompat.dart.DartSdkCompatUtils;
 import com.intellij.openapi.roots.libraries.Library;
 import java.util.function.Predicate;
 import javax.annotation.Nullable;
@@ -28,7 +29,7 @@
   public Predicate<Library> getGcRetentionFilter() {
     return library -> {
       String libraryName = library.getName();
-      return libraryName != null && libraryName.equals(BlazeDartSyncPlugin.DART_SDK_LIBRARY_NAME);
+      return libraryName != null && libraryName.equals(DartSdkCompatUtils.DART_SDK_LIBRARY_NAME);
     };
   }
 }
diff --git a/ijwb/src/com/google/idea/blaze/ijwb/dart/BlazeDartSyncPlugin.java b/ijwb/src/com/google/idea/blaze/ijwb/dart/BlazeDartSyncPlugin.java
index f0049d7..6bc8f5b 100644
--- a/ijwb/src/com/google/idea/blaze/ijwb/dart/BlazeDartSyncPlugin.java
+++ b/ijwb/src/com/google/idea/blaze/ijwb/dart/BlazeDartSyncPlugin.java
@@ -20,17 +20,17 @@
 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.plugin.PluginUtils;
 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.libraries.LibrarySource;
 import com.google.idea.blaze.base.sync.projectview.WorkspaceLanguageSettings;
-import com.google.idea.blaze.ijwb.ide.IdeCheck;
+import com.google.idea.sdkcompat.dart.DartSdkCompatUtils;
 import com.intellij.openapi.module.Module;
 import com.intellij.openapi.project.Project;
 import com.intellij.openapi.roots.ModifiableRootModel;
-import com.intellij.openapi.roots.impl.libraries.ApplicationLibraryTable;
 import com.intellij.openapi.roots.libraries.Library;
 import java.util.Set;
 import javax.annotation.Nullable;
@@ -38,7 +38,6 @@
 /** Supports dart. */
 public class BlazeDartSyncPlugin extends BlazeSyncPlugin.Adapter {
 
-  static final String DART_SDK_LIBRARY_NAME = "Dart SDK";
   private static final String DART_PLUGIN_ID = "Dart";
 
   @Override
@@ -61,8 +60,7 @@
       return;
     }
 
-    Library dartSdkLibrary =
-        ApplicationLibraryTable.getApplicationTable().getLibraryByName(DART_SDK_LIBRARY_NAME);
+    Library dartSdkLibrary = DartSdkCompatUtils.findDartLibrary(project);
     if (dartSdkLibrary != null) {
       if (workspaceModifiableModel.findLibraryOrderEntry(dartSdkLibrary) == null) {
         workspaceModifiableModel.addLibraryEntry(dartSdkLibrary);
@@ -78,14 +76,17 @@
 
   @Override
   public boolean validateProjectView(
+      @Nullable Project project,
       BlazeContext context,
       ProjectViewSet projectViewSet,
       WorkspaceLanguageSettings workspaceLanguageSettings) {
     if (!workspaceLanguageSettings.isLanguageActive(LanguageClass.DART)) {
       return true;
     }
-    if (!IdeCheck.isPluginEnabled(DART_PLUGIN_ID)) {
-      IssueOutput.error("Dart plugin needed for Dart language support.").submit(context);
+    if (!PluginUtils.isPluginEnabled(DART_PLUGIN_ID)) {
+      IssueOutput.error("Dart plugin needed for Dart language support.")
+          .navigatable(PluginUtils.installOrEnablePluginNavigable(DART_PLUGIN_ID))
+          .submit(context);
       return false;
     }
     return true;
@@ -93,7 +94,8 @@
 
   @Nullable
   @Override
-  public LibrarySource getLibrarySource(BlazeProjectData blazeProjectData) {
+  public LibrarySource getLibrarySource(
+      ProjectViewSet projectViewSet, BlazeProjectData blazeProjectData) {
     if (!blazeProjectData.workspaceLanguageSettings.isLanguageActive(LanguageClass.DART)) {
       return null;
     }
diff --git a/ijwb/src/com/google/idea/blaze/ijwb/ide/IdeCheck.java b/ijwb/src/com/google/idea/blaze/ijwb/ide/IdeCheck.java
deleted file mode 100644
index 617b9a7..0000000
--- a/ijwb/src/com/google/idea/blaze/ijwb/ide/IdeCheck.java
+++ /dev/null
@@ -1,35 +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.ijwb.ide;
-
-import com.intellij.ide.plugins.IdeaPluginDescriptor;
-import com.intellij.ide.plugins.PluginManager;
-import com.intellij.openapi.extensions.PluginId;
-
-/** IDE and plugin checks. */
-public class IdeCheck {
-  public static boolean isPluginEnabled(String pluginIdString) {
-    PluginId pluginId = PluginId.getId(pluginIdString);
-    if (!PluginManager.isPluginInstalled(pluginId)) {
-      return false;
-    }
-    IdeaPluginDescriptor plugin = PluginManager.getPlugin(pluginId);
-    if (plugin == null) {
-      return false;
-    }
-    return plugin.isEnabled();
-  }
-}
diff --git a/ijwb/src/com/google/idea/blaze/ijwb/javascript/BlazeJavascriptSyncPlugin.java b/ijwb/src/com/google/idea/blaze/ijwb/javascript/BlazeJavascriptSyncPlugin.java
index c684726..a7c376a 100644
--- a/ijwb/src/com/google/idea/blaze/ijwb/javascript/BlazeJavascriptSyncPlugin.java
+++ b/ijwb/src/com/google/idea/blaze/ijwb/javascript/BlazeJavascriptSyncPlugin.java
@@ -22,6 +22,7 @@
 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.plugin.PluginUtils;
 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;
@@ -46,6 +47,8 @@
 /** Allows people to use a javascript-only workspace. */
 public class BlazeJavascriptSyncPlugin extends BlazeSyncPlugin.Adapter {
 
+  private static final String JAVASCRIPT_PLUGIN_ID = "JavaScript";
+
   @Nullable
   @Override
   public ModuleType getWorkspaceModuleType(WorkspaceType workspaceType) {
@@ -57,12 +60,16 @@
 
   @Override
   public ImmutableList<WorkspaceType> getSupportedWorkspaceTypes() {
-    return ImmutableList.of(WorkspaceType.JAVASCRIPT);
+    return PlatformUtils.isIdeaUltimate()
+        ? ImmutableList.of(WorkspaceType.JAVASCRIPT)
+        : ImmutableList.of();
   }
 
   @Override
   public Set<LanguageClass> getSupportedLanguagesInWorkspace(WorkspaceType workspaceType) {
-    return ImmutableSet.of(LanguageClass.JAVASCRIPT);
+    return PlatformUtils.isIdeaUltimate()
+        ? ImmutableSet.of(LanguageClass.JAVASCRIPT)
+        : ImmutableSet.of();
   }
 
   @Nullable
@@ -114,7 +121,8 @@
 
   @Nullable
   @Override
-  public LibrarySource getLibrarySource(BlazeProjectData blazeProjectData) {
+  public LibrarySource getLibrarySource(
+      ProjectViewSet projectViewSet, BlazeProjectData blazeProjectData) {
     if (!blazeProjectData.workspaceLanguageSettings.isLanguageActive(LanguageClass.JAVASCRIPT)) {
       return null;
     }
@@ -123,16 +131,25 @@
 
   @Override
   public boolean validateProjectView(
+      @Nullable Project project,
       BlazeContext context,
       ProjectViewSet projectViewSet,
       WorkspaceLanguageSettings workspaceLanguageSettings) {
     if (!workspaceLanguageSettings.isLanguageActive(LanguageClass.JAVASCRIPT)) {
       return true;
     }
-    if (!ApplicationManager.getApplication().isUnitTestMode() && !PlatformUtils.isIdeaUltimate()) {
+    if (!PlatformUtils.isIdeaUltimate()) {
       IssueOutput.error("IntelliJ Ultimate needed for Javascript support.").submit(context);
       return false;
     }
+    if (!ApplicationManager.getApplication().isUnitTestMode()
+        && !PluginUtils.isPluginEnabled(JAVASCRIPT_PLUGIN_ID)) {
+      IssueOutput.error(
+              "Javascript support is disabled: please install/enable the JetBrains Javascript "
+                  + "plugin, then restart the IDE")
+          .submit(context);
+      return false;
+    }
     return true;
   }
 }
diff --git a/ijwb/src/com/google/idea/blaze/ijwb/typescript/BlazeTypescriptSyncPlugin.java b/ijwb/src/com/google/idea/blaze/ijwb/typescript/BlazeTypescriptSyncPlugin.java
index e013c14..8d8ab1a 100644
--- a/ijwb/src/com/google/idea/blaze/ijwb/typescript/BlazeTypescriptSyncPlugin.java
+++ b/ijwb/src/com/google/idea/blaze/ijwb/typescript/BlazeTypescriptSyncPlugin.java
@@ -22,6 +22,7 @@
 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.info.BlazeInfo;
 import com.google.idea.blaze.base.ideinfo.TargetMap;
 import com.google.idea.blaze.base.issueparser.IssueOutputLineProcessor;
 import com.google.idea.blaze.base.model.BlazeProjectData;
@@ -42,7 +43,6 @@
 import com.google.idea.blaze.base.sync.libraries.LibrarySource;
 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 com.intellij.openapi.module.Module;
@@ -51,7 +51,9 @@
 import com.intellij.openapi.roots.impl.libraries.ProjectLibraryTable;
 import com.intellij.openapi.roots.libraries.Library;
 import com.intellij.util.PlatformUtils;
+import java.util.ArrayList;
 import java.util.Collection;
+import java.util.LinkedHashSet;
 import java.util.Set;
 import javax.annotation.Nullable;
 
@@ -62,7 +64,9 @@
 
   @Override
   public Set<LanguageClass> getSupportedLanguagesInWorkspace(WorkspaceType workspaceType) {
-    return ImmutableSet.of(LanguageClass.TYPESCRIPT);
+    return PlatformUtils.isIdeaUltimate()
+        ? ImmutableSet.of(LanguageClass.TYPESCRIPT)
+        : ImmutableSet.of();
   }
 
   @Override
@@ -72,7 +76,7 @@
       WorkspaceRoot workspaceRoot,
       ProjectViewSet projectViewSet,
       WorkspaceLanguageSettings workspaceLanguageSettings,
-      BlazeRoots blazeRoots,
+      BlazeInfo blazeInfo,
       @Nullable WorkingSet workingSet,
       WorkspacePathResolver workspacePathResolver,
       ArtifactLocationDecoder artifactLocationDecoder,
@@ -83,8 +87,8 @@
       return;
     }
 
-    Label tsConfig = projectViewSet.getScalarValue(TsConfigRuleSection.KEY);
-    if (tsConfig == null) {
+    Set<Label> tsConfigTargets = getTsConfigTargets(projectViewSet);
+    if (tsConfigTargets.isEmpty()) {
       invalidProjectViewError(context);
       return;
     }
@@ -96,8 +100,10 @@
           childContext.output(new StatusOutput("Updating tsconfig..."));
 
           BlazeCommand command =
-              BlazeCommand.builder(Blaze.getBuildSystem(project), BlazeCommandName.RUN)
-                  .addTargets(tsConfig)
+              BlazeCommand.builder(
+                      Blaze.getBuildSystemProvider(project).getSyncBinaryPath(),
+                      BlazeCommandName.RUN)
+                  .addTargets(new ArrayList<>(tsConfigTargets))
                   .addBlazeFlags(BlazeFlags.buildFlags(project, projectViewSet))
                   .build();
 
@@ -143,6 +149,7 @@
 
   @Override
   public boolean validateProjectView(
+      @Nullable Project project,
       BlazeContext context,
       ProjectViewSet projectViewSet,
       WorkspaceLanguageSettings workspaceLanguageSettings) {
@@ -153,9 +160,8 @@
       return false;
     }
 
-    // Must have either both typescript and ts_config_rule or neither
-    Label tsConfig = projectViewSet.getScalarValue(TsConfigRuleSection.KEY);
-    if (typescriptActive ^ (tsConfig != null)) {
+    // Must have either both typescript and ts_config_rules or neither
+    if (typescriptActive ^ !getTsConfigTargets(projectViewSet).isEmpty()) {
       invalidProjectViewError(context);
       return false;
     }
@@ -165,19 +171,29 @@
 
   private void invalidProjectViewError(BlazeContext context) {
     IssueOutput.error(
-            "For Typescript support you must add both additional_languages: "
-                + "typescript and the ts_config_rule attribute.")
+            "For Typescript support you must add both `additional_languages`: "
+                + "typescript and the `ts_config_rules` attribute.")
         .submit(context);
   }
 
+  private static Set<Label> getTsConfigTargets(ProjectViewSet projectViewSet) {
+    Label oldSectionType = projectViewSet.getScalarValue(TsConfigRuleSection.KEY);
+    Set<Label> labels = new LinkedHashSet<>(projectViewSet.listItems(TsConfigRulesSection.KEY));
+    if (oldSectionType != null) {
+      labels.add(oldSectionType);
+    }
+    return labels;
+  }
+
   @Override
   public Collection<SectionParser> getSections() {
-    return ImmutableList.of(TsConfigRuleSection.PARSER);
+    return ImmutableList.of(TsConfigRuleSection.PARSER, TsConfigRulesSection.PARSER);
   }
 
   @Nullable
   @Override
-  public LibrarySource getLibrarySource(BlazeProjectData blazeProjectData) {
+  public LibrarySource getLibrarySource(
+      ProjectViewSet projectViewSet, BlazeProjectData blazeProjectData) {
     if (!blazeProjectData.workspaceLanguageSettings.isLanguageActive(LanguageClass.TYPESCRIPT)) {
       return null;
     }
diff --git a/ijwb/src/com/google/idea/blaze/ijwb/typescript/TsConfigRuleSection.java b/ijwb/src/com/google/idea/blaze/ijwb/typescript/TsConfigRuleSection.java
index aa3caff..e0f0d5b 100644
--- a/ijwb/src/com/google/idea/blaze/ijwb/typescript/TsConfigRuleSection.java
+++ b/ijwb/src/com/google/idea/blaze/ijwb/typescript/TsConfigRuleSection.java
@@ -28,6 +28,7 @@
 import javax.annotation.Nullable;
 
 /** Points to the ts_config rule. */
+@Deprecated
 public class TsConfigRuleSection {
   public static final SectionKey<Label, ScalarSection<Label>> KEY = SectionKey.of("ts_config_rule");
   public static final SectionParser PARSER = new TsConfigRuleSectionParser();
@@ -45,7 +46,7 @@
         parseContext.addErrors(errors);
         return null;
       }
-      return new Label(rest);
+      return Label.create(rest);
     }
 
     @Override
@@ -57,5 +58,16 @@
     public ItemType getItemType() {
       return ItemType.Label;
     }
+
+    @Override
+    public boolean isDeprecated() {
+      return true;
+    }
+
+    @Nullable
+    @Override
+    public String getDeprecationMessage() {
+      return "Use `ts_config_rules` instead, which allows specifying multiple `ts_config` targets.";
+    }
   }
 }
diff --git a/ijwb/src/com/google/idea/blaze/ijwb/typescript/TsConfigRulesSection.java b/ijwb/src/com/google/idea/blaze/ijwb/typescript/TsConfigRulesSection.java
new file mode 100644
index 0000000..8de392a
--- /dev/null
+++ b/ijwb/src/com/google/idea/blaze/ijwb/typescript/TsConfigRulesSection.java
@@ -0,0 +1,28 @@
+/*
+ * Copyright 2017 The Bazel Authors. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.idea.blaze.ijwb.typescript;
+
+import com.google.idea.blaze.base.model.primitives.Label;
+import com.google.idea.blaze.base.projectview.section.LabelSectionParser;
+import com.google.idea.blaze.base.projectview.section.ListSection;
+import com.google.idea.blaze.base.projectview.section.SectionKey;
+import com.google.idea.blaze.base.projectview.section.SectionParser;
+
+/** Points to 'ts_config' rules. */
+public class TsConfigRulesSection {
+  public static final SectionKey<Label, ListSection<Label>> KEY = SectionKey.of("ts_config_rules");
+  public static final SectionParser PARSER = new LabelSectionParser(KEY);
+}
diff --git a/ijwb/tests/integrationtests/com/google/idea/blaze/ijwb/javascript/JavascriptSyncTest.java b/ijwb/tests/integrationtests/com/google/idea/blaze/ijwb/javascript/JavascriptSyncTest.java
index 5174f96..def1b77 100644
--- a/ijwb/tests/integrationtests/com/google/idea/blaze/ijwb/javascript/JavascriptSyncTest.java
+++ b/ijwb/tests/integrationtests/com/google/idea/blaze/ijwb/javascript/JavascriptSyncTest.java
@@ -18,12 +18,14 @@
 import static com.google.common.truth.Truth.assertThat;
 
 import com.google.common.collect.ImmutableList;
+import com.google.idea.blaze.base.TestUtils;
 import com.google.idea.blaze.base.model.primitives.WorkspacePath;
 import com.google.idea.blaze.base.sync.BlazeSyncIntegrationTestCase;
 import com.google.idea.blaze.base.sync.BlazeSyncParams;
 import com.intellij.openapi.roots.ContentEntry;
 import com.intellij.openapi.roots.SourceFolder;
 import com.intellij.openapi.vfs.VirtualFile;
+import com.intellij.util.PlatformUtils;
 import javax.annotation.Nullable;
 import org.junit.Test;
 import org.junit.runner.RunWith;
@@ -35,6 +37,7 @@
 
   @Test
   public void testSimpleTestSourcesIdentified() {
+    TestUtils.setPlatformPrefix(getTestRootDisposable(), PlatformUtils.IDEA_PREFIX);
     setProjectView(
         "directories:",
         "  common/jslayout/calendar",
@@ -75,6 +78,7 @@
 
   @Test
   public void testTestSourcesMissingFromDirectoriesSectionAreAdded() {
+    TestUtils.setPlatformPrefix(getTestRootDisposable(), PlatformUtils.IDEA_PREFIX);
     setProjectView(
         "directories:",
         "  common/jslayout",
@@ -107,6 +111,7 @@
 
   @Test
   public void testTestSourceChildrenAreNotAddedAsSourceFolders() {
+    TestUtils.setPlatformPrefix(getTestRootDisposable(), PlatformUtils.IDEA_PREFIX);
     // child directories of test sources are always test sources, so they should never
     // appear as separate SourceFolders.
     setProjectView(
@@ -144,6 +149,26 @@
     assertThat(testRoot.isTestSource()).isTrue();
   }
 
+  @Test
+  public void testUsefulErrorMessageInCommunityEdition() {
+    TestUtils.setPlatformPrefix(getTestRootDisposable(), PlatformUtils.IDEA_CE_PREFIX);
+    setProjectView(
+        "directories:",
+        "  common/jslayout",
+        "targets:",
+        "  //common/jslayout/...:all",
+        "workspace_type: javascript");
+
+    workspace.createDirectory(new WorkspacePath("common/jslayout"));
+
+    BlazeSyncParams syncParams =
+        new BlazeSyncParams.Builder("Full Sync", BlazeSyncParams.SyncMode.FULL)
+            .addProjectViewTargets(true)
+            .build();
+    runBlazeSync(syncParams);
+    errorCollector.assertIssues("IntelliJ Ultimate needed for Javascript support.");
+  }
+
   @Nullable
   private static SourceFolder findSourceFolder(ContentEntry entry, VirtualFile file) {
     for (SourceFolder sourceFolder : entry.getSourceFolders()) {
diff --git a/ijwb/tests/integrationtests/com/google/idea/blaze/ijwb/lang/projectview/ProjectViewCompletionTest.java b/ijwb/tests/integrationtests/com/google/idea/blaze/ijwb/lang/projectview/ProjectViewCompletionTest.java
new file mode 100644
index 0000000..401f132
--- /dev/null
+++ b/ijwb/tests/integrationtests/com/google/idea/blaze/ijwb/lang/projectview/ProjectViewCompletionTest.java
@@ -0,0 +1,68 @@
+/*
+ * Copyright 2017 The Bazel Authors. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.idea.blaze.ijwb.lang.projectview;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.google.common.base.Joiner;
+import com.google.idea.blaze.base.BlazeIntegrationTestCase;
+import com.google.idea.blaze.base.EditorTestHelper;
+import com.google.idea.blaze.base.model.MockBlazeProjectDataBuilder;
+import com.google.idea.blaze.base.model.MockBlazeProjectDataManager;
+import com.google.idea.blaze.base.model.primitives.LanguageClass;
+import com.google.idea.blaze.base.model.primitives.WorkspaceType;
+import com.google.idea.blaze.base.sync.data.BlazeProjectDataManager;
+import com.google.idea.blaze.base.sync.projectview.LanguageSupport;
+import com.intellij.psi.PsiFile;
+import java.util.stream.Collectors;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** Project view completion tests requiring IJwB's sync plugins to be present. */
+@RunWith(JUnit4.class)
+public class ProjectViewCompletionTest extends BlazeIntegrationTestCase {
+
+  protected EditorTestHelper editorTest;
+
+  @Before
+  public final void doSetup() {
+    BlazeProjectDataManager mockProjectDataManager =
+        new MockBlazeProjectDataManager(MockBlazeProjectDataBuilder.builder(workspaceRoot).build());
+    registerProjectService(BlazeProjectDataManager.class, mockProjectDataManager);
+    editorTest = new EditorTestHelper(getProject(), testFixture);
+  }
+
+  private PsiFile setInput(String... fileContents) {
+    return testFixture.configureByText(".blazeproject", Joiner.on("\n").join(fileContents));
+  }
+
+  @Test
+  public void testAdditionalLanguagesCompletion() {
+    setInput("additional_languages:", "  <caret>");
+
+    String[] types = editorTest.getCompletionItemsAsStrings();
+
+    assertThat(types)
+        .asList()
+        .containsAllIn(
+            LanguageSupport.availableAdditionalLanguages(WorkspaceType.JAVA)
+                .stream()
+                .map(LanguageClass::getName)
+                .collect(Collectors.toList()));
+  }
+}
diff --git a/ijwb/tests/unittests/com/google/idea/blaze/ijwb/android/BlazeAndroidLiteSyncPluginTest.java b/ijwb/tests/unittests/com/google/idea/blaze/ijwb/android/BlazeAndroidLiteSyncPluginTest.java
index 9f3dfd9..aa90aa5 100644
--- a/ijwb/tests/unittests/com/google/idea/blaze/ijwb/android/BlazeAndroidLiteSyncPluginTest.java
+++ b/ijwb/tests/unittests/com/google/idea/blaze/ijwb/android/BlazeAndroidLiteSyncPluginTest.java
@@ -87,7 +87,7 @@
                     .build())
             .build();
     WorkspaceLanguageSettings workspaceLanguageSettings =
-        LanguageSupport.createWorkspaceLanguageSettings(context, projectViewSet);
+        LanguageSupport.createWorkspaceLanguageSettings(projectViewSet);
     errorCollector.assertNoIssues();
     assertThat(workspaceLanguageSettings)
         .isEqualTo(
diff --git a/ijwb/tests/unittests/com/google/idea/blaze/ijwb/dart/BlazeDartSyncPluginTest.java b/ijwb/tests/unittests/com/google/idea/blaze/ijwb/dart/BlazeDartSyncPluginTest.java
index 2c00838..d8a27e3 100644
--- a/ijwb/tests/unittests/com/google/idea/blaze/ijwb/dart/BlazeDartSyncPluginTest.java
+++ b/ijwb/tests/unittests/com/google/idea/blaze/ijwb/dart/BlazeDartSyncPluginTest.java
@@ -86,7 +86,7 @@
                     .build())
             .build();
     WorkspaceLanguageSettings workspaceLanguageSettings =
-        LanguageSupport.createWorkspaceLanguageSettings(context, projectViewSet);
+        LanguageSupport.createWorkspaceLanguageSettings(projectViewSet);
     errorCollector.assertNoIssues();
     assertThat(workspaceLanguageSettings)
         .isEqualTo(
diff --git a/ijwb/tests/unittests/com/google/idea/blaze/ijwb/javascript/BlazeJavascriptSyncPluginTest.java b/ijwb/tests/unittests/com/google/idea/blaze/ijwb/javascript/BlazeJavascriptSyncPluginTest.java
index b90851e..da42baf 100644
--- a/ijwb/tests/unittests/com/google/idea/blaze/ijwb/javascript/BlazeJavascriptSyncPluginTest.java
+++ b/ijwb/tests/unittests/com/google/idea/blaze/ijwb/javascript/BlazeJavascriptSyncPluginTest.java
@@ -19,6 +19,7 @@
 
 import com.google.common.collect.ImmutableSet;
 import com.google.idea.blaze.base.BlazeTestCase;
+import com.google.idea.blaze.base.TestUtils;
 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;
@@ -34,7 +35,7 @@
 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 org.jetbrains.annotations.NotNull;
+import com.intellij.util.PlatformUtils;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.junit.runners.JUnit4;
@@ -47,8 +48,7 @@
   private BlazeContext context;
 
   @Override
-  protected void initTest(
-      @NotNull Container applicationServices, @NotNull Container projectServices) {
+  protected void initTest(Container applicationServices, Container projectServices) {
     super.initTest(applicationServices, projectServices);
 
     ExtensionPointImpl<BlazeSyncPlugin> ep =
@@ -60,7 +60,8 @@
   }
 
   @Test
-  public void testJavascriptLanguageAvailable() {
+  public void testJavascriptLanguageAvailableForUltimateEdition() {
+    TestUtils.setPlatformPrefix(testDisposable, PlatformUtils.IDEA_PREFIX);
     ProjectViewSet projectViewSet =
         ProjectViewSet.builder()
             .add(
@@ -74,7 +75,7 @@
                     .build())
             .build();
     WorkspaceLanguageSettings workspaceLanguageSettings =
-        LanguageSupport.createWorkspaceLanguageSettings(context, projectViewSet);
+        LanguageSupport.createWorkspaceLanguageSettings(projectViewSet);
     errorCollector.assertNoIssues();
     assertThat(workspaceLanguageSettings)
         .isEqualTo(
@@ -82,4 +83,22 @@
                 WorkspaceType.JAVASCRIPT,
                 ImmutableSet.of(LanguageClass.JAVASCRIPT, LanguageClass.GENERIC)));
   }
+
+  @Test
+  public void testJavascriptWorkspaceTypeUnavailableForCommunityEdition() {
+    TestUtils.setPlatformPrefix(testDisposable, PlatformUtils.IDEA_CE_PREFIX);
+    ProjectViewSet projectViewSet =
+        ProjectViewSet.builder()
+            .add(
+                ProjectView.builder()
+                    .add(
+                        ScalarSection.builder(WorkspaceTypeSection.KEY)
+                            .set(WorkspaceType.JAVASCRIPT))
+                    .build())
+            .build();
+    WorkspaceLanguageSettings workspaceLanguageSettings =
+        LanguageSupport.createWorkspaceLanguageSettings(projectViewSet);
+    LanguageSupport.validateLanguageSettings(context, workspaceLanguageSettings);
+    errorCollector.assertIssues("Workspace type 'javascript' is not supported by this plugin");
+  }
 }
diff --git a/ijwb/tests/unittests/com/google/idea/blaze/ijwb/typescript/BlazeTypescriptSyncPluginTest.java b/ijwb/tests/unittests/com/google/idea/blaze/ijwb/typescript/BlazeTypescriptSyncPluginTest.java
index e39fa49..1975ae8 100644
--- a/ijwb/tests/unittests/com/google/idea/blaze/ijwb/typescript/BlazeTypescriptSyncPluginTest.java
+++ b/ijwb/tests/unittests/com/google/idea/blaze/ijwb/typescript/BlazeTypescriptSyncPluginTest.java
@@ -20,6 +20,7 @@
 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.TestUtils;
 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;
@@ -35,6 +36,7 @@
 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 com.intellij.util.PlatformUtils;
 import java.util.Set;
 import org.jetbrains.annotations.NotNull;
 import org.junit.Test;
@@ -75,7 +77,8 @@
   }
 
   @Test
-  public void testTypescriptLanguageAvailable() {
+  public void testTypescriptLanguageAvailableInUltimateEdition() {
+    TestUtils.setPlatformPrefix(testDisposable, PlatformUtils.IDEA_PREFIX);
     ProjectViewSet projectViewSet =
         ProjectViewSet.builder()
             .add(
@@ -87,7 +90,7 @@
                     .build())
             .build();
     WorkspaceLanguageSettings workspaceLanguageSettings =
-        LanguageSupport.createWorkspaceLanguageSettings(context, projectViewSet);
+        LanguageSupport.createWorkspaceLanguageSettings(projectViewSet);
     errorCollector.assertNoIssues();
     assertThat(workspaceLanguageSettings)
         .isEqualTo(
@@ -96,4 +99,23 @@
                 ImmutableSet.of(
                     LanguageClass.TYPESCRIPT, LanguageClass.GENERIC, LanguageClass.JAVA)));
   }
+
+  @Test
+  public void testTypescriptNotLanguageAvailableInCommunityEdition() {
+    TestUtils.setPlatformPrefix(testDisposable, PlatformUtils.IDEA_CE_PREFIX);
+    ProjectViewSet projectViewSet =
+        ProjectViewSet.builder()
+            .add(
+                ProjectView.builder()
+                    .add(ScalarSection.builder(WorkspaceTypeSection.KEY).set(WorkspaceType.JAVA))
+                    .add(
+                        ListSection.builder(AdditionalLanguagesSection.KEY)
+                            .add(LanguageClass.TYPESCRIPT))
+                    .build())
+            .build();
+    WorkspaceLanguageSettings workspaceLanguageSettings =
+        LanguageSupport.createWorkspaceLanguageSettings(projectViewSet);
+    LanguageSupport.validateLanguageSettings(context, workspaceLanguageSettings);
+    errorCollector.assertIssues("Language 'typescript' is not supported by this plugin");
+  }
 }
diff --git a/intellij_platform_sdk/BUILD b/intellij_platform_sdk/BUILD
index 9edcbbf..d20389e 100644
--- a/intellij_platform_sdk/BUILD
+++ b/intellij_platform_sdk/BUILD
@@ -16,6 +16,21 @@
     },
 )
 
+config_setting(
+    name = "intellij-beta",
+    values = {
+        "define": "ij_product=intellij-beta",
+    },
+)
+
+# IntelliJ CE 2017.1.1
+config_setting(
+    name = "intellij-2017.1.1",
+    values = {
+        "define": "ij_product=intellij-2017.1.1",
+    },
+)
+
 # IntelliJ CE 2016.3.1
 config_setting(
     name = "intellij-2016.3.1",
@@ -24,14 +39,6 @@
     },
 )
 
-# IntelliJ CE 2016.2.4
-config_setting(
-    name = "intellij-162.2032.8",
-    values = {
-        "define": "ij_product=intellij-162.2032.8",
-    },
-)
-
 config_setting(
     name = "android-studio-latest",
     values = {
@@ -39,14 +46,6 @@
     },
 )
 
-# Android Studio 2.2.0.7
-config_setting(
-    name = "android-studio-145.1617.8",
-    values = {
-        "define": "ij_product=android-studio-145.1617.8",
-    },
-)
-
 config_setting(
     name = "android-studio-beta",
     values = {
@@ -54,19 +53,19 @@
     },
 )
 
-# Android Studio 2.3.0.3
+# Android Studio 2.3.0.8
 config_setting(
-    name = "android-studio-2.3.0.3",
+    name = "android-studio-2.3.0.8",
     values = {
-        "define": "ij_product=android-studio-2.3.0.3",
+        "define": "ij_product=android-studio-2.3.0.8",
     },
 )
 
-# Android Studio 2.3.0.4
+# Android Studio 2.3.1.0
 config_setting(
-    name = "android-studio-2.3.0.4",
+    name = "android-studio-2.3.1.0",
     values = {
-        "define": "ij_product=android-studio-2.3.0.4",
+        "define": "ij_product=android-studio-2.3.1.0",
     },
 )
 
@@ -77,6 +76,21 @@
     },
 )
 
+config_setting(
+    name = "clion-beta",
+    values = {
+        "define": "ij_product=clion-beta",
+    },
+)
+
+# CLion 2017.1.1
+config_setting(
+    name = "clion-2017.1.1",
+    values = {
+        "define": "ij_product=clion-2017.1.1",
+    },
+)
+
 # CLion 2016.3.2
 config_setting(
     name = "clion-2016.3.2",
@@ -178,6 +192,28 @@
     ),
 )
 
+# IntelliJ tasks plugin
+java_library(
+    name = "tasks",
+    neverlink = 1,
+    exports = select_from_plugin_api_directory(
+        android_studio = [":tasks"],
+        clion = [":tasks"],
+        intellij = [":tasks"],
+    ),
+)
+
+# terminal plugin
+java_library(
+    name = "terminal",
+    neverlink = 1,
+    exports = select_from_plugin_api_directory(
+        android_studio = [],
+        clion = [":terminal"],
+        intellij = [":terminal"],
+    ),
+)
+
 # Bundled plugins required by integration tests
 java_library(
     name = "bundled_plugins",
@@ -195,10 +231,10 @@
 java_library(
     name = "missing_test_classes",
     srcs = select_for_plugin_api({
-        "android-studio-2.3.0.3": [
+        "android-studio-2.3.0.8": [
             "missing/tests/com/jetbrains/cidr/modulemap/resolve/MockModuleMapManagerImpl.java",
         ],
-        "android-studio-2.3.0.4": [
+        "android-studio-2.3.1.0": [
             "missing/tests/com/jetbrains/cidr/modulemap/resolve/MockModuleMapManagerImpl.java",
         ],
         "default": [],
diff --git a/intellij_platform_sdk/BUILD.clion b/intellij_platform_sdk/BUILD.clion
index c83e150..d43b8a1 100644
--- a/intellij_platform_sdk/BUILD.clion
+++ b/intellij_platform_sdk/BUILD.clion
@@ -15,6 +15,16 @@
     jars = glob(["clion-*/plugins/hg4idea/lib/hg4idea.jar"]),
 )
 
+java_import(
+    name = "terminal",
+    jars = glob(["clion-*/plugins/terminal/lib/terminal.jar"]),
+)
+
+java_import(
+    name = "python",
+    jars = glob(["clion-*/plugins/python/lib/python.jar"]),
+)
+
 # The plugins required by CLwB. Presumably there will be some, when we write
 # some integration tests.
 java_import(
diff --git a/intellij_platform_sdk/BUILD.idea b/intellij_platform_sdk/BUILD.idea
index 32a92ad..4133e09 100644
--- a/intellij_platform_sdk/BUILD.idea
+++ b/intellij_platform_sdk/BUILD.idea
@@ -25,6 +25,11 @@
     jars = glob(["plugins/junit/lib/*.jar"]),
 )
 
+java_import(
+    name = "terminal",
+    jars = ["plugins/terminal/lib/terminal.jar"],
+)
+
 # The plugins required by IJwB. We need to include them
 # when running integration tests.
 java_import(
diff --git a/intellij_platform_sdk/build_defs.bzl b/intellij_platform_sdk/build_defs.bzl
index 6fa3efb..5c1bbda 100644
--- a/intellij_platform_sdk/build_defs.bzl
+++ b/intellij_platform_sdk/build_defs.bzl
@@ -2,32 +2,30 @@
 
 # The current indirect ij_product mapping (eg. "intellij-latest")
 INDIRECT_IJ_PRODUCTS = {
-    "intellij-latest": "intellij-2016.3.1",
-    "android-studio-latest": "android-studio-145.1617.8",
-    "android-studio-beta": "android-studio-2.3.0.4",
-    "clion-latest": "clion-162.1967.7",
+    "intellij-latest": "intellij-2017.1.1",
+    "intellij-beta": "intellij-2017.1.1",
+    "android-studio-latest": "android-studio-2.3.1.0",
+    "android-studio-beta": "android-studio-2.3.1.0",
+    "clion-latest": "clion-2017.1.1",
+    "clion-beta": "clion-2017.1.1",
 }
 
 DIRECT_IJ_PRODUCTS = {
+    "intellij-2017.1.1": struct(
+        ide="intellij",
+        directory="intellij_ce_2017_1_1",
+    ),
     "intellij-2016.3.1": struct(
         ide="intellij",
         directory="intellij_ce_2016_3_1",
     ),
-    "intellij-162.2032.8": struct(
-        ide="intellij",
-        directory="IC_162_2032_8",
-    ),
-    "android-studio-145.1617.8": struct(
+    "android-studio-2.3.0.8": struct(
         ide="android-studio",
-        directory="AI_145_1617_8",
+        directory="android_studio_2_3_0_8",
     ),
-    "android-studio-2.3.0.3": struct(
+    "android-studio-2.3.1.0": struct(
         ide="android-studio",
-        directory="android_studio_2_3_0_3",
-    ),
-    "android-studio-2.3.0.4": struct(
-        ide="android-studio",
-        directory="android_studio_2_3_0_4",
+        directory="android_studio_2_3_1_0",
     ),
     "clion-162.1967.7": struct(
         ide="clion",
@@ -37,15 +35,12 @@
         ide="clion",
         directory="clion_2016_3_2",
     ),
+    "clion-2017.1.1": struct(
+        ide="clion",
+        directory="clion_2017_1_1",
+    ),
 }
 
-# BUILD_VARS for each IDE corresponding to indirect ij_products, eg. "intellij-latest"
-
-
-
-
-
-
 def select_for_plugin_api(params):
   """Selects for a plugin_api.
 
@@ -99,7 +94,7 @@
 
   return select(select_params)
 
-def select_for_ide(intellij=None, android_studio=None, clion=None, default=None):
+def select_for_ide(intellij=None, android_studio=None, clion=None, default=[]):
   """Selects for the supported IDEs.
 
   Args:
@@ -108,7 +103,7 @@
       clion: Files to use for CLion. If None will use default.
       default: Files to use for any IDEs not passed.
   Returns:
-      A select statement on all plugin_apis, sorted into IDEs.
+      A select statement on all plugin_apis to lists of files, sorted into IDEs.
 
   Example:
     java_library(
@@ -119,9 +114,9 @@
       ),
     )
   """
-  intellij = intellij or default
-  android_studio = android_studio or default
-  clion = clion or default
+  intellij = intellij if intellij != None else default
+  android_studio = android_studio if android_studio != None else default
+  clion = clion if clion != None else default
 
   ide_to_value = {
       "intellij" : intellij,
diff --git a/java/BUILD b/java/BUILD
index ac9f772..fb36558 100644
--- a/java/BUILD
+++ b/java/BUILD
@@ -1,16 +1,30 @@
 licenses(["notice"])  # Apache 2.0
 
+load(
+    "//build_defs:build_defs.bzl",
+    "merged_plugin_xml",
+    "stamped_plugin_xml",
+    "intellij_plugin",
+)
+load(
+    "//testing:test_defs.bzl",
+    "intellij_integration_test_suite",
+    "intellij_unit_test_suite",
+)
+
 java_library(
     name = "java",
     srcs = glob(["src/**/*.java"]),
     visibility = ["//visibility:public"],
-    deps = [
-        "//base",
+    runtime_deps = [
         "//common/actionhelper",
         "//common/experiments",
+    ],
+    deps = [
+        "//base",
         "//intellij_platform_sdk:junit",
         "//intellij_platform_sdk:plugin_api",
-        "//proto_deps",
+        "//proto:proto_deps",
         "//sdkcompat",
         "@jsr305_annotations//jar",
     ],
@@ -22,13 +36,6 @@
     visibility = ["//visibility:public"],
 )
 
-load(
-    "//build_defs:build_defs.bzl",
-    "merged_plugin_xml",
-    "stamped_plugin_xml",
-    "intellij_plugin",
-)
-
 merged_plugin_xml(
     name = "merged_plugin_xml",
     srcs = [
@@ -54,12 +61,6 @@
     ],
 )
 
-load(
-    "//testing:test_defs.bzl",
-    "intellij_integration_test_suite",
-    "intellij_unit_test_suite",
-)
-
 intellij_unit_test_suite(
     name = "unit_tests",
     srcs = glob(["tests/unittests/**/*.java"]),
@@ -71,7 +72,7 @@
         "//common/experiments",
         "//common/experiments:unit_test_utils",
         "//intellij_platform_sdk:plugin_api_for_tests",
-        "//proto_deps",
+        "//proto:proto_deps",
         "@jsr305_annotations//jar",
         "@junit//jar",
     ],
diff --git a/java/src/META-INF/blaze-java.xml b/java/src/META-INF/blaze-java.xml
index e46fa6c..e22c895 100644
--- a/java/src/META-INF/blaze-java.xml
+++ b/java/src/META-INF/blaze-java.xml
@@ -69,9 +69,10 @@
     <BlazeUserSettingsContributor implementation="com.google.idea.blaze.java.settings.BlazeJavaUserSettingsContributor$BlazeJavaUserSettingsProvider"/>
     <FileCache implementation="com.google.idea.blaze.java.libraries.JarCache$FileCacheAdapter"/>
     <PrefetchFileSource implementation="com.google.idea.blaze.java.sync.JavaPrefetchFileSource"/>
-    <SyncListener implementation="com.google.idea.blaze.java.syncstatus.SyncStatusHelper$UpdateSyncStatusMap"/>
     <BlazeTestEventsHandler implementation="com.google.idea.blaze.java.run.BlazeJavaTestEventsHandler"/>
     <AttributeSpecificStringLiteralReferenceProvider implementation="com.google.idea.blaze.java.lang.build.references.JavaClassQualifiedNameReference"/>
+    <JavaLikeLanguage implementation="com.google.idea.blaze.java.sync.source.JavaLikeLanguage$Java"/>
+    <TestTargetHeuristic implementation="com.google.idea.blaze.java.run.JUnitTestHeuristic" order="before TestSizeHeuristic"/>
   </extensions>
 
   <extensions defaultExtensionNs="com.intellij">
@@ -84,6 +85,12 @@
     <runConfigurationProducer
         implementation="com.google.idea.blaze.java.run.producers.BlazeJavaTestMethodConfigurationProducer"
         order="first"/>
+    <runConfigurationProducer
+        implementation="com.google.idea.blaze.java.run.producers.BlazeJavaAbstractTestCaseConfigurationProducer"
+        order="first"/>
+    <runConfigurationProducer
+        implementation="com.google.idea.blaze.java.run.producers.MultipleJavaClassesTestConfigurationProducer"
+        order="first"/>
     <projectViewNodeDecorator implementation="com.google.idea.blaze.java.syncstatus.BlazeJavaSyncStatusClassNodeDecorator"/>
     <editorTabColorProvider implementation="com.google.idea.blaze.java.syncstatus.BlazeJavaSyncStatusEditorTabColorProvider"/>
     <editorTabTitleProvider implementation="com.google.idea.blaze.java.syncstatus.BlazeJavaSyncStatusEditorTabTitleProvider"/>
@@ -105,7 +112,6 @@
     <attachSourcesProvider implementation="com.google.idea.blaze.java.libraries.AddLibraryTargetDirectoryToProjectViewAttachSourcesProvider"/>
     <attachSourcesProvider implementation="com.google.idea.blaze.java.libraries.BlazeAttachSourceProvider"/>
     <applicationService serviceImplementation="com.google.idea.blaze.java.settings.BlazeJavaUserSettings"/>
-    <projectService serviceImplementation="com.google.idea.blaze.java.syncstatus.SyncStatusHelper"/>
     <psi.referenceContributor language="BUILD" implementation="com.google.idea.blaze.java.lang.build.references.JavaClassReferenceContributor"/>
   </extensions>
 
@@ -116,7 +122,11 @@
   </project-components>
 
   <extensionPoints>
-    <extensionPoint qualifiedName="com.google.idea.blaze.java.JavaSyncAugmenter"
+    <extensionPoint qualifiedName="com.google.idea.blaze.JavaSyncAugmenter"
                     interface="com.google.idea.blaze.java.sync.BlazeJavaSyncAugmenter"/>
+    <extensionPoint qualifiedName="com.google.idea.blaze.JavaLikeLanguage"
+                    interface="com.google.idea.blaze.java.sync.source.JavaLikeLanguage"/>
+    <extensionPoint qualifiedName="com.google.idea.blaze.JUnitParameterizedClassHeuristic"
+                    interface="com.google.idea.blaze.java.run.producers.JUnitParameterizedClassHeuristic"/>
   </extensionPoints>
 </idea-plugin>
diff --git a/java/src/com/google/idea/blaze/java/lang/build/BuildFileSafeDeleteProcessor.java b/java/src/com/google/idea/blaze/java/lang/build/BuildFileSafeDeleteProcessor.java
index 390ce7b..be94716 100644
--- a/java/src/com/google/idea/blaze/java/lang/build/BuildFileSafeDeleteProcessor.java
+++ b/java/src/com/google/idea/blaze/java/lang/build/BuildFileSafeDeleteProcessor.java
@@ -28,10 +28,9 @@
 import com.intellij.usageView.UsageInfo;
 import com.intellij.util.IncorrectOperationException;
 import java.util.Collection;
-import java.util.Iterator;
 import java.util.List;
+import javax.annotation.Nullable;
 import org.jetbrains.annotations.NotNull;
-import org.jetbrains.annotations.Nullable;
 
 /**
  * Removes glob references which don't refer directly to the item(s) being deleted (b/28979434)
@@ -55,12 +54,7 @@
       @NotNull PsiElement[] allElementsToDelete,
       @NotNull List<UsageInfo> result) {
     NonCodeUsageSearchInfo superResult = super.findUsages(element, allElementsToDelete, result);
-    Iterator<UsageInfo> iter = result.iterator();
-    while (iter.hasNext()) {
-      if (ignoreUsage(iter.next())) {
-        iter.remove();
-      }
-    }
+    result.removeIf(BuildFileSafeDeleteProcessor::ignoreUsage);
     return superResult;
   }
 
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 ec05bb7..c5e45c6 100644
--- a/java/src/com/google/idea/blaze/java/libraries/AddLibraryTargetDirectoryToProjectViewAction.java
+++ b/java/src/com/google/idea/blaze/java/libraries/AddLibraryTargetDirectoryToProjectViewAction.java
@@ -21,6 +21,7 @@
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.Sets;
 import com.google.idea.blaze.base.actions.BlazeProjectAction;
+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.model.BlazeProjectData;
@@ -86,7 +87,7 @@
     if (blazeLibrary == null) {
       return null;
     }
-    TargetKey originatingTarget = blazeLibrary.originatingTarget;
+    TargetKey originatingTarget = findOriginatingTargetForLibrary(blazeProjectData, blazeLibrary);
     if (originatingTarget == null) {
       return null;
     }
@@ -123,6 +124,21 @@
     return workspacePath;
   }
 
+  @Nullable
+  private static TargetKey findOriginatingTargetForLibrary(
+      BlazeProjectData blazeProjectData, BlazeJarLibrary library) {
+    for (TargetIdeInfo target : blazeProjectData.targetMap.targets()) {
+      JavaIdeInfo javaIdeInfo = target.javaIdeInfo;
+      if (javaIdeInfo == null) {
+        continue;
+      }
+      if (javaIdeInfo.jars.contains(library.libraryArtifact)) {
+        return target.key;
+      }
+    }
+    return null;
+  }
+
   static void addDirectoriesToProjectView(Project project, List<Library> libraries) {
     Set<WorkspacePath> workspacePaths = Sets.newHashSet();
     for (Library library : libraries) {
diff --git a/java/src/com/google/idea/blaze/java/libraries/JarCache.java b/java/src/com/google/idea/blaze/java/libraries/JarCache.java
index 8617b8c..66eafaa 100644
--- a/java/src/com/google/idea/blaze/java/libraries/JarCache.java
+++ b/java/src/com/google/idea/blaze/java/libraries/JarCache.java
@@ -78,8 +78,12 @@
   }
 
   public void onSync(
-      BlazeContext context, BlazeProjectData projectData, BlazeSyncParams.SyncMode syncMode) {
-    Collection<BlazeLibrary> libraries = BlazeLibraryCollector.getLibraries(projectData);
+      BlazeContext context,
+      ProjectViewSet projectViewSet,
+      BlazeProjectData projectData,
+      BlazeSyncParams.SyncMode syncMode) {
+    Collection<BlazeLibrary> libraries =
+        BlazeLibraryCollector.getLibraries(projectViewSet, projectData);
     boolean fullRefresh = syncMode == SyncMode.FULL;
     boolean removeMissingFiles = syncMode == SyncMode.INCREMENTAL;
     boolean enabled = updateEnabled();
@@ -326,7 +330,7 @@
         ProjectViewSet projectViewSet,
         BlazeProjectData projectData,
         BlazeSyncParams.SyncMode syncMode) {
-      getInstance(project).onSync(context, projectData, syncMode);
+      getInstance(project).onSync(context, projectViewSet, projectData, syncMode);
     }
 
     @Override
diff --git a/java/src/com/google/idea/blaze/java/libraries/LibraryActionHelper.java b/java/src/com/google/idea/blaze/java/libraries/LibraryActionHelper.java
index 35a00ca..cc50651 100644
--- a/java/src/com/google/idea/blaze/java/libraries/LibraryActionHelper.java
+++ b/java/src/com/google/idea/blaze/java/libraries/LibraryActionHelper.java
@@ -31,8 +31,8 @@
 import com.intellij.openapi.ui.Messages;
 import com.intellij.openapi.util.text.StringUtil;
 import com.intellij.pom.Navigatable;
+import javax.annotation.Nullable;
 import org.jetbrains.annotations.NotNull;
-import org.jetbrains.annotations.Nullable;
 
 class LibraryActionHelper {
 
diff --git a/java/src/com/google/idea/blaze/java/run/BlazeJavaDebuggerRunner.java b/java/src/com/google/idea/blaze/java/run/BlazeJavaDebuggerRunner.java
index 33a6540..140b08d 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.google.idea.sdkcompat.debugger.GenericDebuggerRunnerSdkCompatAdapter;
+import com.intellij.debugger.impl.GenericDebuggerRunner;
 import com.intellij.execution.ExecutionException;
 import com.intellij.execution.configurations.JavaParameters;
 import com.intellij.execution.configurations.RemoteConnection;
@@ -30,7 +30,7 @@
 import javax.annotation.Nullable;
 
 /** A runner that adapts the GenericDebuggerRunner to work with Blaze run configurations. */
-public class BlazeJavaDebuggerRunner extends GenericDebuggerRunnerSdkCompatAdapter {
+public class BlazeJavaDebuggerRunner extends GenericDebuggerRunner {
 
   // wait 10 minutes for the blaze build to complete before connecting
   private static final long POLL_TIMEOUT_MILLIS = 10 * 60 * 1000;
diff --git a/java/src/com/google/idea/blaze/java/run/BlazeJavaRunConfigurationFactory.java b/java/src/com/google/idea/blaze/java/run/BlazeJavaRunConfigurationFactory.java
index 3b7c1b5..3daee7a 100644
--- a/java/src/com/google/idea/blaze/java/run/BlazeJavaRunConfigurationFactory.java
+++ b/java/src/com/google/idea/blaze/java/run/BlazeJavaRunConfigurationFactory.java
@@ -50,7 +50,7 @@
     BlazeCommandRunConfigurationCommonState state =
         blazeConfig.getHandlerStateIfType(BlazeCommandRunConfigurationCommonState.class);
     if (state != null) {
-      state.setCommand(BlazeCommandName.RUN);
+      state.getCommandState().setCommand(BlazeCommandName.RUN);
     }
     blazeConfig.setGeneratedName();
   }
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 479b964..109d56e 100644
--- a/java/src/com/google/idea/blaze/java/run/BlazeJavaRunConfigurationHandler.java
+++ b/java/src/com/google/idea/blaze/java/run/BlazeJavaRunConfigurationHandler.java
@@ -72,7 +72,7 @@
   @Override
   @Nullable
   public String getCommandName() {
-    BlazeCommandName command = state.getCommand();
+    BlazeCommandName command = state.getCommandState().getCommand();
     return command != null ? command.toString() : null;
   }
 
diff --git a/java/src/com/google/idea/blaze/java/run/BlazeJavaRunProfileState.java b/java/src/com/google/idea/blaze/java/run/BlazeJavaRunProfileState.java
index 6175856..e7c4773 100644
--- a/java/src/com/google/idea/blaze/java/run/BlazeJavaRunProfileState.java
+++ b/java/src/com/google/idea/blaze/java/run/BlazeJavaRunProfileState.java
@@ -40,7 +40,6 @@
 import com.google.idea.blaze.base.settings.Blaze;
 import com.google.idea.blaze.base.settings.BlazeImportSettings;
 import com.google.idea.blaze.base.settings.BlazeImportSettingsManager;
-import com.google.idea.common.experiments.BoolExperiment;
 import com.intellij.execution.DefaultExecutionResult;
 import com.intellij.execution.ExecutionException;
 import com.intellij.execution.ExecutionResult;
@@ -65,9 +64,6 @@
  */
 final class BlazeJavaRunProfileState extends CommandLineState implements RemoteState {
 
-  private static final BoolExperiment smRunnerUiEnabled =
-      new BoolExperiment("use.smrunner.ui.java", true);
-
   // Blaze seems to always use this port for --java_debug.
   // TODO(joshgiles): Look at manually identifying and setting port.
   private static final int DEBUG_PORT = 5005;
@@ -155,14 +151,11 @@
   }
 
   private boolean useTestUi() {
-    if (!smRunnerUiEnabled.getValue()) {
-      return false;
-    }
     BlazeCommandRunConfigurationCommonState state =
         configuration.getHandlerStateIfType(BlazeCommandRunConfigurationCommonState.class);
     return state != null
-        && BlazeCommandName.TEST.equals(state.getCommand())
-        && !state.getRunOnDistributedExecutor();
+        && BlazeCommandName.TEST.equals(state.getCommandState().getCommand())
+        && !state.getRunOnDistributedExecutorState().runOnDistributedExecutor;
   }
 
   @Override
@@ -171,10 +164,10 @@
       return null;
     }
     return new RemoteConnection(
-        true /* useSockets */,
+        /* useSockets */ true,
         DEBUG_HOST_NAME,
         Integer.toString(DEBUG_PORT),
-        false /* serverMode */);
+        /* serverMode */ false);
   }
 
   @VisibleForTesting
@@ -189,15 +182,19 @@
         configuration.getHandlerStateIfType(BlazeCommandRunConfigurationCommonState.class);
     assert handlerState != null;
 
-    BlazeCommandName blazeCommand = handlerState.getCommand();
+    String binaryPath =
+        handlerState.getBlazeBinaryState().getBlazeBinary() != null
+            ? handlerState.getBlazeBinaryState().getBlazeBinary()
+            : Blaze.getBuildSystemProvider(project).getBinaryPath();
+
+    BlazeCommandName blazeCommand = handlerState.getCommandState().getCommand();
     assert blazeCommand != null;
     BlazeCommand.Builder command =
-        BlazeCommand.builder(Blaze.getBuildSystem(project), blazeCommand)
-            .setBlazeBinary(handlerState.getBlazeBinary())
+        BlazeCommand.builder(binaryPath, blazeCommand)
             .addTargets(configuration.getTarget())
             .addBlazeFlags(BlazeFlags.buildFlags(project, projectViewSet))
             .addBlazeFlags(extraBlazeFlags)
-            .addBlazeFlags(handlerState.getBlazeFlags());
+            .addBlazeFlags(handlerState.getBlazeFlagsState().getExpandedFlags());
 
     if (debug) {
       Kind kind = configuration.getKindForTarget();
@@ -208,14 +205,12 @@
         command.addBlazeFlags(BlazeFlags.JAVA_TEST_DEBUG);
       }
     } else {
-      boolean runDistributed = handlerState.getRunOnDistributedExecutor();
+      boolean runDistributed =
+          handlerState.getRunOnDistributedExecutorState().runOnDistributedExecutor;
       command.addBlazeFlags(DistributedExecutorSupport.getBlazeFlags(project, runDistributed));
-      if (!runDistributed) {
-        command.addBlazeFlags(BlazeFlags.TEST_OUTPUT_STREAMED);
-      }
     }
 
-    command.addExeFlags(handlerState.getExeFlags());
+    command.addExeFlags(handlerState.getExeFlagsState().getExpandedFlags());
     return command.build();
   }
 }
diff --git a/java/src/com/google/idea/blaze/java/run/BlazeJavaTestEventsHandler.java b/java/src/com/google/idea/blaze/java/run/BlazeJavaTestEventsHandler.java
index 4a1024f..8091b51 100644
--- a/java/src/com/google/idea/blaze/java/run/BlazeJavaTestEventsHandler.java
+++ b/java/src/com/google/idea/blaze/java/run/BlazeJavaTestEventsHandler.java
@@ -47,13 +47,13 @@
 
   /** Overridden to support parameterized tests, which use nested test_suite XML elements. */
   @Override
-  public boolean ignoreSuite(TestSuite suite) {
+  public boolean ignoreSuite(@Nullable Kind kind, TestSuite suite) {
     if (suite.testSuites.isEmpty()) {
       return false;
     }
     for (TestSuite child : suite.testSuites) {
       // target/class names are fully-qualified; unqualified names denote parameterized methods
-      if (!child.name.contains(".")) {
+      if (child.name != null && !child.name.contains(".")) {
         return false;
       }
     }
diff --git a/java/src/com/google/idea/blaze/java/run/BlazeJavaTestRunConfigurationFactory.java b/java/src/com/google/idea/blaze/java/run/BlazeJavaTestRunConfigurationFactory.java
index 007aba3..9b9aad8 100644
--- a/java/src/com/google/idea/blaze/java/run/BlazeJavaTestRunConfigurationFactory.java
+++ b/java/src/com/google/idea/blaze/java/run/BlazeJavaTestRunConfigurationFactory.java
@@ -50,7 +50,7 @@
     BlazeCommandRunConfigurationCommonState state =
         blazeConfig.getHandlerStateIfType(BlazeCommandRunConfigurationCommonState.class);
     if (state != null) {
-      state.setCommand(BlazeCommandName.TEST);
+      state.getCommandState().setCommand(BlazeCommandName.TEST);
     }
     blazeConfig.setGeneratedName();
   }
diff --git a/java/src/com/google/idea/blaze/java/run/JUnitTestHeuristic.java b/java/src/com/google/idea/blaze/java/run/JUnitTestHeuristic.java
new file mode 100644
index 0000000..dae9416
--- /dev/null
+++ b/java/src/com/google/idea/blaze/java/run/JUnitTestHeuristic.java
@@ -0,0 +1,71 @@
+/*
+ * Copyright 2017 The Bazel Authors. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.idea.blaze.java.run;
+
+import com.google.idea.blaze.base.ideinfo.TargetIdeInfo;
+import com.google.idea.blaze.base.ideinfo.TestIdeInfo.TestSize;
+import com.google.idea.blaze.base.run.TestTargetHeuristic;
+import com.google.idea.blaze.java.run.producers.BlazeJUnitTestFilterFlags.JUnitVersion;
+import com.intellij.execution.junit.JUnitUtil;
+import com.intellij.openapi.project.Project;
+import com.intellij.psi.PsiClass;
+import com.intellij.psi.PsiClassOwner;
+import com.intellij.psi.PsiFile;
+import java.io.File;
+import javax.annotation.Nullable;
+
+/** Matches junit test sources to test targets with junit3/junit4 in their name. */
+public class JUnitTestHeuristic implements TestTargetHeuristic {
+
+  @Override
+  public boolean matchesSource(
+      Project project,
+      TargetIdeInfo target,
+      @Nullable PsiFile sourcePsiFile,
+      File sourceFile,
+      @Nullable TestSize testSize) {
+    JUnitVersion sourceVersion = junitVersion(sourcePsiFile);
+    if (sourceVersion == null) {
+      return false;
+    }
+    String targetName = target.key.label.targetName().toString().toLowerCase();
+    switch (sourceVersion) {
+      case JUNIT_4:
+        return targetName.contains("junit4");
+      case JUNIT_3:
+        return targetName.contains("junit3");
+    }
+    return false;
+  }
+
+  @Nullable
+  private JUnitVersion junitVersion(@Nullable PsiFile psiFile) {
+    if (!(psiFile instanceof PsiClassOwner)) {
+      return null;
+    }
+    for (PsiClass psiClass : ((PsiClassOwner) psiFile).getClasses()) {
+      if (JUnitUtil.isJUnit4TestClass(psiClass)) {
+        return JUnitVersion.JUNIT_4;
+      }
+    }
+    for (PsiClass psiClass : ((PsiClassOwner) psiFile).getClasses()) {
+      if (JUnitUtil.isJUnit3TestClass(psiClass)) {
+        return JUnitVersion.JUNIT_3;
+      }
+    }
+    return null;
+  }
+}
diff --git a/java/src/com/google/idea/blaze/java/run/RunUtil.java b/java/src/com/google/idea/blaze/java/run/RunUtil.java
index 724d72f..3985700 100644
--- a/java/src/com/google/idea/blaze/java/run/RunUtil.java
+++ b/java/src/com/google/idea/blaze/java/run/RunUtil.java
@@ -16,7 +16,7 @@
 package com.google.idea.blaze.java.run;
 
 import com.google.idea.blaze.base.ideinfo.TargetIdeInfo;
-import com.google.idea.blaze.base.ideinfo.TestIdeInfo;
+import com.google.idea.blaze.base.ideinfo.TestIdeInfo.TestSize;
 import com.google.idea.blaze.base.model.primitives.Label;
 import com.google.idea.blaze.base.run.TestTargetFinder;
 import com.google.idea.blaze.base.run.TestTargetHeuristic;
@@ -27,8 +27,7 @@
 import com.intellij.psi.PsiFile;
 import java.io.File;
 import java.util.Collection;
-import org.jetbrains.annotations.NotNull;
-import org.jetbrains.annotations.Nullable;
+import javax.annotation.Nullable;
 
 /** Utility methods for finding rules and Android facets. */
 public final class RunUtil {
@@ -40,18 +39,17 @@
    *     containing rules, the first rule sorted alphabetically by label.
    */
   @Nullable
-  public static TargetIdeInfo targetForTestClass(
-      @NotNull Project project,
-      @NotNull PsiClass testClass,
-      @Nullable TestIdeInfo.TestSize testSize) {
+  public static TargetIdeInfo targetForTestClass(PsiClass testClass, @Nullable TestSize testSize) {
     File testFile = getFileForClass(testClass);
     if (testFile == null) {
       return null;
     }
+    Project project = testClass.getProject();
     Collection<TargetIdeInfo> targets =
         TestTargetFinder.getInstance(project).testTargetsForSourceFile(testFile);
     Label testLabel =
-        TestTargetHeuristic.chooseTestTargetForSourceFile(testFile, targets, testSize);
+        TestTargetHeuristic.chooseTestTargetForSourceFile(
+            project, testClass.getContainingFile(), testFile, targets, testSize);
     if (testLabel == null) {
       return null;
     }
@@ -64,7 +62,7 @@
    * memory.
    */
   @Nullable
-  public static File getFileForClass(@NotNull PsiClass aClass) {
+  public static File getFileForClass(PsiClass aClass) {
     PsiFile containingFile = aClass.getContainingFile();
     if (containingFile == null) {
       return null;
diff --git a/java/src/com/google/idea/blaze/java/run/producers/BlazeJUnitTestFilterFlags.java b/java/src/com/google/idea/blaze/java/run/producers/BlazeJUnitTestFilterFlags.java
index 4ee0508..79090d1 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
@@ -78,7 +78,8 @@
   }
 
   private static boolean isParameterized(PsiClass testClass) {
-    return PsiMemberParameterizedLocation.getParameterizedLocation(testClass, null) != null;
+    return PsiMemberParameterizedLocation.getParameterizedLocation(testClass, null) != null
+        || JUnitParameterizedClassHeuristic.isParameterizedTest(testClass);
   }
 
   private static String methodFilter(PsiMethod method, boolean parameterizedClass) {
@@ -105,10 +106,12 @@
       String filter =
           testFilterForClassAndMethods(
               entry.getKey(), version, extractMethodFilters(entry.getValue()));
-      if (filter != null) {
-        classFilters.add(filter);
+      if (filter == null) {
+        return null;
       }
+      classFilters.add(filter);
     }
+    classFilters.sort(String::compareTo);
     return version == JUnitVersion.JUNIT_4
         ? String.join("|", classFilters)
         : String.join(",", classFilters);
diff --git a/java/src/com/google/idea/blaze/java/run/producers/BlazeJavaAbstractTestCaseConfigurationProducer.java b/java/src/com/google/idea/blaze/java/run/producers/BlazeJavaAbstractTestCaseConfigurationProducer.java
new file mode 100644
index 0000000..d9fb30b
--- /dev/null
+++ b/java/src/com/google/idea/blaze/java/run/producers/BlazeJavaAbstractTestCaseConfigurationProducer.java
@@ -0,0 +1,225 @@
+/*
+ * Copyright 2017 The Bazel Authors. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.idea.blaze.java.run.producers;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.collect.ImmutableList;
+import com.google.idea.blaze.base.command.BlazeCommandName;
+import com.google.idea.blaze.base.command.BlazeFlags;
+import com.google.idea.blaze.base.ideinfo.TargetIdeInfo;
+import com.google.idea.blaze.base.ideinfo.TestIdeInfo;
+import com.google.idea.blaze.base.run.BlazeCommandRunConfiguration;
+import com.google.idea.blaze.base.run.BlazeCommandRunConfigurationType;
+import com.google.idea.blaze.base.run.BlazeConfigurationNameBuilder;
+import com.google.idea.blaze.base.run.producers.BlazeRunConfigurationProducer;
+import com.google.idea.blaze.base.run.smrunner.SmRunnerUtils;
+import com.google.idea.blaze.base.run.state.BlazeCommandRunConfigurationCommonState;
+import com.google.idea.blaze.java.run.RunUtil;
+import com.intellij.codeInsight.AnnotationUtil;
+import com.intellij.execution.JavaExecutionUtil;
+import com.intellij.execution.Location;
+import com.intellij.execution.actions.ConfigurationContext;
+import com.intellij.execution.actions.ConfigurationFromContext;
+import com.intellij.execution.configurations.RunConfiguration;
+import com.intellij.execution.junit.JUnitUtil;
+import com.intellij.openapi.util.Ref;
+import com.intellij.psi.PsiClass;
+import com.intellij.psi.PsiElement;
+import com.intellij.psi.PsiMethod;
+import com.intellij.psi.PsiModifier;
+import com.intellij.psi.util.PsiTreeUtil;
+import java.util.ArrayList;
+import java.util.List;
+import javax.annotation.Nullable;
+
+/** Producer for abstract test classes/methods. */
+public class BlazeJavaAbstractTestCaseConfigurationProducer
+    extends BlazeRunConfigurationProducer<BlazeCommandRunConfiguration> {
+
+  private static class AbstractTestLocation {
+    private final PsiClass abstractClass;
+    @Nullable private final PsiMethod method;
+
+    private AbstractTestLocation(PsiClass abstractClass, @Nullable PsiMethod method) {
+      this.abstractClass = abstractClass;
+      this.method = method;
+    }
+  }
+
+  public BlazeJavaAbstractTestCaseConfigurationProducer() {
+    super(BlazeCommandRunConfigurationType.getInstance());
+  }
+
+  @Override
+  protected boolean doSetupConfigFromContext(
+      BlazeCommandRunConfiguration configuration,
+      ConfigurationContext context,
+      Ref<PsiElement> sourceElement) {
+    AbstractTestLocation location = getAbstractLocation(context);
+    if (location == null) {
+      return false;
+    }
+    sourceElement.set(location.method != null ? location.method : location.abstractClass);
+    configuration.setName(configName(location.abstractClass, location.method));
+    configuration.setNameChangedByUser(true);
+    return true;
+  }
+
+  @Nullable
+  private static AbstractTestLocation getAbstractLocation(ConfigurationContext context) {
+    if (!SmRunnerUtils.getSelectedSmRunnerTreeElements(context).isEmpty()) {
+      // handled by a different producer
+      return null;
+    }
+    PsiMethod method = getTestMethod(context);
+    if (method != null) {
+      PsiClass psiClass = method.getContainingClass();
+      return isAbstractClass(psiClass) ? new AbstractTestLocation(psiClass, method) : null;
+    }
+    Location location = context.getLocation();
+    if (location == null) {
+      return null;
+    }
+    location = JavaExecutionUtil.stepIntoSingleClass(location);
+    if (location == null) {
+      return null;
+    }
+    PsiClass psiClass =
+        PsiTreeUtil.getParentOfType(location.getPsiElement(), PsiClass.class, false);
+    if (!isAbstractClass(psiClass)) {
+      return null;
+    }
+    for (PsiClass subclass : SubclassTestChooser.findTestSubclasses(psiClass)) {
+      if (JUnitUtil.isTestClass(subclass)) {
+        return new AbstractTestLocation(psiClass, null);
+      }
+    }
+    return null;
+  }
+
+  private static PsiMethod getTestMethod(ConfigurationContext context) {
+    PsiElement psi = context.getPsiLocation();
+    if (psi instanceof PsiMethod
+        && AnnotationUtil.isAnnotated((PsiMethod) psi, JUnitUtil.TEST_ANNOTATION, false)) {
+      return (PsiMethod) psi;
+    }
+    List<PsiMethod> selectedMethods = TestMethodSelectionUtil.getSelectedMethods(context);
+    return selectedMethods != null && selectedMethods.size() == 1 ? selectedMethods.get(0) : null;
+  }
+
+  private static boolean isAbstractClass(@Nullable PsiClass psiClass) {
+    return psiClass != null && psiClass.hasModifierProperty(PsiModifier.ABSTRACT);
+  }
+
+  @Override
+  protected boolean doIsConfigFromContext(
+      BlazeCommandRunConfiguration configuration, ConfigurationContext context) {
+    // this is an intermediate type -- when it's fully instantiated (via 'onFirstRun') it will be
+    // recognized by a different producer.
+    return false;
+  }
+
+  @Override
+  public void onFirstRun(
+      ConfigurationFromContext configuration,
+      ConfigurationContext context,
+      Runnable startRunnable) {
+    chooseSubclass(configuration, context, startRunnable);
+  }
+
+  @VisibleForTesting
+  static void chooseSubclass(
+      ConfigurationFromContext configuration,
+      ConfigurationContext context,
+      Runnable startRunnable) {
+    RunConfiguration config = configuration.getConfiguration();
+    if (!(config instanceof BlazeCommandRunConfiguration)) {
+      return;
+    }
+    AbstractTestLocation location = locationFromConfiguration(configuration);
+    if (location == null) {
+      return;
+    }
+    SubclassTestChooser.chooseSubclass(
+        context,
+        location.abstractClass,
+        (psiClass) -> {
+          if (psiClass != null) {
+            setupContext((BlazeCommandRunConfiguration) config, psiClass, location.method);
+          }
+          startRunnable.run();
+        });
+  }
+
+  @Nullable
+  private static AbstractTestLocation locationFromConfiguration(
+      ConfigurationFromContext configuration) {
+    PsiElement element = configuration.getSourceElement();
+    PsiMethod method = null;
+    PsiClass psiClass = null;
+    if (element instanceof PsiMethod) {
+      method = (PsiMethod) element;
+      psiClass = method.getContainingClass();
+    } else if (element instanceof PsiClass) {
+      psiClass = (PsiClass) element;
+    }
+
+    return isAbstractClass(psiClass) ? new AbstractTestLocation(psiClass, method) : null;
+  }
+
+  private static void setupContext(
+      BlazeCommandRunConfiguration configuration, PsiClass subClass, @Nullable PsiMethod method) {
+    TestIdeInfo.TestSize testSize =
+        method != null
+            ? TestSizeAnnotationMap.getTestSize(method)
+            : TestSizeAnnotationMap.getTestSize(subClass);
+    TargetIdeInfo target = RunUtil.targetForTestClass(subClass, testSize);
+    if (target == null) {
+      return;
+    }
+    configuration.setTarget(target.key.label);
+    BlazeCommandRunConfigurationCommonState handlerState =
+        configuration.getHandlerStateIfType(BlazeCommandRunConfigurationCommonState.class);
+    if (handlerState == null) {
+      return;
+    }
+    handlerState.getCommandState().setCommand(BlazeCommandName.TEST);
+
+    // remove old test filter flag if present
+    List<String> flags = new ArrayList<>(handlerState.getBlazeFlagsState().getRawFlags());
+    flags.removeIf((flag) -> flag.startsWith(BlazeFlags.TEST_FILTER));
+
+    String testFilter =
+        BlazeJUnitTestFilterFlags.testFilterForClassAndMethods(
+            subClass, method == null ? ImmutableList.of() : ImmutableList.of(method));
+    if (testFilter == null) {
+      return;
+    }
+    flags.add(BlazeFlags.TEST_FILTER + "=" + testFilter);
+    handlerState.getBlazeFlagsState().setRawFlags(flags);
+
+    BlazeConfigurationNameBuilder nameBuilder = new BlazeConfigurationNameBuilder(configuration);
+    nameBuilder.setTargetString(configName(subClass, method));
+    configuration.setName(nameBuilder.build());
+    configuration.setNameChangedByUser(true); // don't revert to generated name
+  }
+
+  private static String configName(PsiClass psiClass, @Nullable PsiMethod method) {
+    return method == null
+        ? psiClass.getName()
+        : String.format("%s.%s", psiClass.getName(), method.getName());
+  }
+}
diff --git a/java/src/com/google/idea/blaze/java/run/producers/BlazeJavaMainClassRunConfigurationProducer.java b/java/src/com/google/idea/blaze/java/run/producers/BlazeJavaMainClassRunConfigurationProducer.java
index b90826a..0399176 100644
--- a/java/src/com/google/idea/blaze/java/run/producers/BlazeJavaMainClassRunConfigurationProducer.java
+++ b/java/src/com/google/idea/blaze/java/run/producers/BlazeJavaMainClassRunConfigurationProducer.java
@@ -41,7 +41,7 @@
 import java.io.File;
 import java.util.Collection;
 import java.util.Objects;
-import org.jetbrains.annotations.Nullable;
+import javax.annotation.Nullable;
 
 /** Creates run configurations for Java main classes sourced by java_binary targets. */
 public class BlazeJavaMainClassRunConfigurationProducer
@@ -81,7 +81,7 @@
     if (handlerState == null) {
       return false;
     }
-    handlerState.setCommand(BlazeCommandName.RUN);
+    handlerState.getCommandState().setCommand(BlazeCommandName.RUN);
     configuration.setGeneratedName();
     return true;
   }
@@ -94,7 +94,7 @@
     if (handlerState == null) {
       return false;
     }
-    if (!Objects.equals(handlerState.getCommand(), BlazeCommandName.RUN)) {
+    if (!Objects.equals(handlerState.getCommandState().getCommand(), BlazeCommandName.RUN)) {
       return false;
     }
     PsiClass mainClass = getMainClass(context);
diff --git a/java/src/com/google/idea/blaze/java/run/producers/BlazeJavaTestClassConfigurationProducer.java b/java/src/com/google/idea/blaze/java/run/producers/BlazeJavaTestClassConfigurationProducer.java
index 7d9a20f..1609e30 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
@@ -15,6 +15,7 @@
  */
 package com.google.idea.blaze.java.run.producers;
 
+import com.google.common.collect.ImmutableList;
 import com.google.idea.blaze.base.command.BlazeCommandName;
 import com.google.idea.blaze.base.command.BlazeFlags;
 import com.google.idea.blaze.base.ideinfo.TargetIdeInfo;
@@ -36,8 +37,13 @@
 import com.intellij.psi.PsiMethod;
 import com.intellij.psi.PsiModifier;
 import java.util.ArrayList;
+import java.util.Collection;
 import java.util.List;
+import java.util.Map;
 import java.util.Objects;
+import java.util.Set;
+import java.util.stream.Collectors;
+import javax.annotation.Nullable;
 import org.jetbrains.annotations.NotNull;
 
 /** Producer for run configurations related to Java test classes in Blaze. */
@@ -79,7 +85,7 @@
     sourceElement.set(testClass);
 
     TestIdeInfo.TestSize testSize = TestSizeAnnotationMap.getTestSize(testClass);
-    TargetIdeInfo target = RunUtil.targetForTestClass(context.getProject(), testClass, testSize);
+    TargetIdeInfo target = RunUtil.targetForTestClass(testClass, testSize);
     if (target == null) {
       return false;
     }
@@ -90,17 +96,17 @@
     if (handlerState == null) {
       return false;
     }
-    String testFilter = BlazeJUnitTestFilterFlags.testFilterForClass(testClass);
+    String testFilter = getTestFilter(testClass);
     if (testFilter == null) {
       return false;
     }
-    handlerState.setCommand(BlazeCommandName.TEST);
+    handlerState.getCommandState().setCommand(BlazeCommandName.TEST);
 
     // remove old test filter flag if present
-    List<String> flags = new ArrayList<>(handlerState.getBlazeFlags());
+    List<String> flags = new ArrayList<>(handlerState.getBlazeFlagsState().getRawFlags());
     flags.removeIf((flag) -> flag.startsWith(BlazeFlags.TEST_FILTER));
     flags.add(BlazeFlags.TEST_FILTER + "=" + testFilter);
-    handlerState.setBlazeFlags(flags);
+    handlerState.getBlazeFlagsState().setRawFlags(flags);
 
     BlazeConfigurationNameBuilder nameBuilder = new BlazeConfigurationNameBuilder(configuration);
     nameBuilder.setTargetString(testClass.getName());
@@ -148,13 +154,25 @@
     if (handlerState == null) {
       return false;
     }
-    if (!Objects.equals(handlerState.getCommand(), BlazeCommandName.TEST)) {
+    if (!Objects.equals(handlerState.getCommandState().getCommand(), BlazeCommandName.TEST)) {
       return false;
     }
-    String filter = BlazeJUnitTestFilterFlags.testFilterForClass(testClass);
+    String filter = getTestFilter(testClass);
     if (filter == null) {
       return false;
     }
     return Objects.equals(BlazeFlags.TEST_FILTER + "=" + filter, handlerState.getTestFilterFlag());
   }
+
+  @Nullable
+  private static String getTestFilter(PsiClass testClass) {
+    Set<PsiClass> innerTestClasses = ProducerUtils.getInnerTestClasses(testClass);
+    if (innerTestClasses.isEmpty()) {
+      return BlazeJUnitTestFilterFlags.testFilterForClass(testClass);
+    }
+    innerTestClasses.add(testClass);
+    Map<PsiClass, Collection<Location<?>>> methodsPerClass =
+        innerTestClasses.stream().collect(Collectors.toMap(c -> c, c -> ImmutableList.of()));
+    return BlazeJUnitTestFilterFlags.testFilterForClassesAndMethods(methodsPerClass);
+  }
 }
diff --git a/java/src/com/google/idea/blaze/java/run/producers/BlazeJavaTestMethodConfigurationProducer.java b/java/src/com/google/idea/blaze/java/run/producers/BlazeJavaTestMethodConfigurationProducer.java
index e0453d1..b1cabb3 100644
--- a/java/src/com/google/idea/blaze/java/run/producers/BlazeJavaTestMethodConfigurationProducer.java
+++ b/java/src/com/google/idea/blaze/java/run/producers/BlazeJavaTestMethodConfigurationProducer.java
@@ -80,8 +80,7 @@
     sourceElement.set(methodInfo.firstMethod);
 
     TestIdeInfo.TestSize testSize = TestSizeAnnotationMap.getTestSize(methodInfo.firstMethod);
-    TargetIdeInfo target =
-        RunUtil.targetForTestClass(context.getProject(), methodInfo.containingClass, testSize);
+    TargetIdeInfo target = RunUtil.targetForTestClass(methodInfo.containingClass, testSize);
     if (target == null) {
       return false;
     }
@@ -92,13 +91,16 @@
     if (handlerState == null) {
       return false;
     }
-    handlerState.setCommand(BlazeCommandName.TEST);
+    handlerState.getCommandState().setCommand(BlazeCommandName.TEST);
 
     // remove old test filter flag if present
-    List<String> flags = new ArrayList<>(handlerState.getBlazeFlags());
+    List<String> flags = new ArrayList<>(handlerState.getBlazeFlagsState().getRawFlags());
     flags.removeIf((flag) -> flag.startsWith(BlazeFlags.TEST_FILTER));
     flags.add(methodInfo.testFilterFlag);
-    handlerState.setBlazeFlags(flags);
+    if (!flags.contains(BlazeFlags.DISABLE_TEST_SHARDING)) {
+      flags.add(BlazeFlags.DISABLE_TEST_SHARDING);
+    }
+    handlerState.getBlazeFlagsState().setRawFlags(flags);
 
     BlazeConfigurationNameBuilder nameBuilder = new BlazeConfigurationNameBuilder(configuration);
     nameBuilder.setTargetString(
@@ -118,7 +120,7 @@
     if (handlerState == null) {
       return false;
     }
-    if (!Objects.equals(handlerState.getCommand(), BlazeCommandName.TEST)) {
+    if (!Objects.equals(handlerState.getCommandState().getCommand(), BlazeCommandName.TEST)) {
       return false;
     }
 
@@ -127,7 +129,7 @@
       return false;
     }
 
-    List<String> flags = handlerState.getBlazeFlags();
+    List<String> flags = handlerState.getBlazeFlagsState().getRawFlags();
     return flags.contains(methodInfo.testFilterFlag);
   }
 
diff --git a/java/src/com/google/idea/blaze/java/run/producers/JUnitConfigurationUtil.java b/java/src/com/google/idea/blaze/java/run/producers/JUnitConfigurationUtil.java
index b994e36..f316d6e 100644
--- a/java/src/com/google/idea/blaze/java/run/producers/JUnitConfigurationUtil.java
+++ b/java/src/com/google/idea/blaze/java/run/producers/JUnitConfigurationUtil.java
@@ -18,8 +18,6 @@
 import com.intellij.execution.Location;
 import com.intellij.execution.actions.ConfigurationContext;
 import com.intellij.execution.junit.JUnitUtil;
-import com.intellij.execution.junit2.PsiMemberParameterizedLocation;
-import com.intellij.execution.junit2.info.MethodLocation;
 import com.intellij.execution.testframework.TestsUIUtil;
 import com.intellij.openapi.actionSystem.CommonDataKeys;
 import com.intellij.openapi.actionSystem.DataContext;
@@ -33,14 +31,11 @@
 import com.intellij.psi.PsiElement;
 import com.intellij.psi.PsiFile;
 import com.intellij.psi.PsiManager;
-import com.intellij.psi.PsiMember;
 import com.intellij.psi.PsiMethod;
 import com.intellij.psi.PsiModifier;
 import com.intellij.psi.PsiPackage;
 import com.intellij.psi.search.PsiElementProcessor;
-import com.intellij.psi.util.ClassUtil;
 import java.util.ArrayList;
-import java.util.LinkedHashSet;
 import java.util.List;
 
 /**
@@ -48,10 +43,6 @@
  * isMultipleElementsSelected.
  */
 public class JUnitConfigurationUtil {
-  protected static boolean isTestClass(PsiClass psiClass) {
-    return JUnitUtil.isTestClass(psiClass);
-  }
-
   protected static boolean isTestMethod(boolean checkAbstract, PsiElement psiElement) {
     return JUnitUtil.getTestMethod(psiElement, checkAbstract) != null;
   }
@@ -61,14 +52,13 @@
     if (TestsUIUtil.isMultipleSelectionImpossible(dataContext)) {
       return false;
     }
-    final LinkedHashSet<String> classes = new LinkedHashSet<String>();
     final PsiElementProcessor.CollectElementsWithLimit<PsiElement> processor =
         new PsiElementProcessor.CollectElementsWithLimit<PsiElement>(2);
-    final PsiElement[] locationElements = collectLocationElements(classes, dataContext);
+    final PsiElement[] locationElements = collectLocationElements(dataContext);
     if (locationElements != null) {
       collectTestMembers(locationElements, false, false, processor);
     } else {
-      collectContextElements(dataContext, false, false, classes, processor);
+      collectContextElements(dataContext, false, false, processor);
     }
     return processor.getCollection().size() > 1;
   }
@@ -83,14 +73,14 @@
         final PsiClass[] classes = ((PsiClassOwner) psiElement).getClasses();
         for (PsiClass aClass : classes) {
           if ((!checkIsTest && aClass.hasModifierProperty(PsiModifier.PUBLIC)
-                  || checkIsTest && isTestClass(aClass))
+                  || checkIsTest && JUnitUtil.isTestClass(aClass))
               && !collectingProcessor.execute(aClass)) {
             return;
           }
         }
       } else if (psiElement instanceof PsiClass) {
         if ((!checkIsTest && ((PsiClass) psiElement).hasModifierProperty(PsiModifier.PUBLIC)
-                || checkIsTest && isTestClass((PsiClass) psiElement))
+                || checkIsTest && JUnitUtil.isTestClass((PsiClass) psiElement))
             && !collectingProcessor.execute(psiElement)) {
           return;
         }
@@ -122,14 +112,10 @@
       DataContext dataContext,
       boolean checkAbstract,
       boolean checkIsTest,
-      LinkedHashSet<String> classes,
       PsiElementProcessor.CollectElements<PsiElement> processor) {
     PsiElement[] elements = LangDataKeys.PSI_ELEMENT_ARRAY.getData(dataContext);
     if (elements != null) {
       collectTestMembers(elements, checkAbstract, checkIsTest, processor);
-      for (PsiElement psiClass : processor.getCollection()) {
-        classes.add(getQName(psiClass));
-      }
       return true;
     } else {
       final VirtualFile[] files = CommonDataKeys.VIRTUAL_FILE_ARRAY.getData(dataContext);
@@ -142,9 +128,6 @@
             if (psiFile instanceof PsiClassOwner) {
               collectTestMembers(
                   ((PsiClassOwner) psiFile).getClasses(), checkAbstract, checkIsTest, processor);
-              for (PsiElement psiMember : processor.getCollection()) {
-                classes.add(((PsiClass) psiMember).getQualifiedName());
-              }
             }
           }
           return true;
@@ -154,41 +137,16 @@
     return false;
   }
 
-  private static PsiElement[] collectLocationElements(
-      LinkedHashSet<String> classes, DataContext dataContext) {
+  private static PsiElement[] collectLocationElements(DataContext dataContext) {
     final Location<?>[] locations = Location.DATA_KEYS.getData(dataContext);
     if (locations != null) {
       List<PsiElement> elements = new ArrayList<PsiElement>();
       for (Location<?> location : locations) {
         final PsiElement psiElement = location.getPsiElement();
-        classes.add(getQName(psiElement, location));
         elements.add(psiElement);
       }
       return elements.toArray(new PsiElement[elements.size()]);
     }
     return null;
   }
-
-  public static String getQName(PsiElement psiMember) {
-    return getQName(psiMember, null);
-  }
-
-  public static String getQName(PsiElement psiMember, Location location) {
-    if (psiMember instanceof PsiClass) {
-      return ClassUtil.getJVMClassName((PsiClass) psiMember);
-    } else if (psiMember instanceof PsiMember) {
-      final PsiClass containingClass =
-          location instanceof MethodLocation
-              ? ((MethodLocation) location).getContainingClass()
-              : location instanceof PsiMemberParameterizedLocation
-                  ? ((PsiMemberParameterizedLocation) location).getContainingClass()
-                  : ((PsiMember) psiMember).getContainingClass();
-      assert containingClass != null;
-      return ClassUtil.getJVMClassName(containingClass) + "," + ((PsiMember) psiMember).getName();
-    } else if (psiMember instanceof PsiPackage) {
-      return ((PsiPackage) psiMember).getQualifiedName();
-    }
-    assert false;
-    return null;
-  }
 }
diff --git a/java/src/com/google/idea/blaze/java/run/producers/JUnitParameterizedClassHeuristic.java b/java/src/com/google/idea/blaze/java/run/producers/JUnitParameterizedClassHeuristic.java
new file mode 100644
index 0000000..cc995c5
--- /dev/null
+++ b/java/src/com/google/idea/blaze/java/run/producers/JUnitParameterizedClassHeuristic.java
@@ -0,0 +1,37 @@
+/*
+ * Copyright 2017 The Bazel Authors. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.idea.blaze.java.run.producers;
+
+import com.intellij.openapi.extensions.ExtensionPointName;
+import com.intellij.psi.PsiClass;
+
+/** A heuristic to recognize JUnit test runner classes which have parameterized test cases. */
+public interface JUnitParameterizedClassHeuristic {
+
+  ExtensionPointName<JUnitParameterizedClassHeuristic> EP_NAME =
+      ExtensionPointName.create("com.google.idea.blaze.JUnitParameterizedClassHeuristic");
+
+  static boolean isParameterizedTest(PsiClass psiClass) {
+    for (JUnitParameterizedClassHeuristic heuristic : EP_NAME.getExtensions()) {
+      if (heuristic.isParameterized(psiClass)) {
+        return true;
+      }
+    }
+    return false;
+  }
+
+  boolean isParameterized(PsiClass psiClass);
+}
diff --git a/java/src/com/google/idea/blaze/java/run/producers/MultipleJavaClassesTestConfigurationProducer.java b/java/src/com/google/idea/blaze/java/run/producers/MultipleJavaClassesTestConfigurationProducer.java
new file mode 100644
index 0000000..b2f5e83
--- /dev/null
+++ b/java/src/com/google/idea/blaze/java/run/producers/MultipleJavaClassesTestConfigurationProducer.java
@@ -0,0 +1,249 @@
+/*
+ * Copyright 2017 The Bazel Authors. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.idea.blaze.java.run.producers;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableSet;
+import com.google.idea.blaze.base.command.BlazeCommandName;
+import com.google.idea.blaze.base.command.BlazeFlags;
+import com.google.idea.blaze.base.ideinfo.TargetIdeInfo;
+import com.google.idea.blaze.base.ideinfo.TestIdeInfo;
+import com.google.idea.blaze.base.lang.buildfile.search.BlazePackage;
+import com.google.idea.blaze.base.model.primitives.Label;
+import com.google.idea.blaze.base.run.BlazeCommandRunConfiguration;
+import com.google.idea.blaze.base.run.BlazeCommandRunConfigurationType;
+import com.google.idea.blaze.base.run.BlazeConfigurationNameBuilder;
+import com.google.idea.blaze.base.run.producers.BlazeRunConfigurationProducer;
+import com.google.idea.blaze.base.run.smrunner.SmRunnerUtils;
+import com.google.idea.blaze.base.run.state.BlazeCommandRunConfigurationCommonState;
+import com.google.idea.blaze.java.run.RunUtil;
+import com.intellij.execution.Location;
+import com.intellij.execution.actions.ConfigurationContext;
+import com.intellij.execution.junit.JUnitUtil;
+import com.intellij.openapi.actionSystem.DataContext;
+import com.intellij.openapi.actionSystem.LangDataKeys;
+import com.intellij.openapi.roots.ProjectFileIndex;
+import com.intellij.openapi.util.Ref;
+import com.intellij.psi.JavaDirectoryService;
+import com.intellij.psi.PsiClass;
+import com.intellij.psi.PsiDirectory;
+import com.intellij.psi.PsiElement;
+import com.intellij.psi.PsiModifier;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Set;
+import java.util.stream.Collectors;
+import javax.annotation.Nullable;
+
+/**
+ * Runs tests in all selected java classes (or all classes below selected directory). Ignores
+ * classes spread across multiple test targets.
+ */
+public class MultipleJavaClassesTestConfigurationProducer
+    extends BlazeRunConfigurationProducer<BlazeCommandRunConfiguration> {
+
+  public MultipleJavaClassesTestConfigurationProducer() {
+    super(BlazeCommandRunConfigurationType.getInstance());
+  }
+
+  @Override
+  protected boolean doSetupConfigFromContext(
+      BlazeCommandRunConfiguration configuration,
+      ConfigurationContext context,
+      Ref<PsiElement> sourceElement) {
+    TestLocation location = getTestLocation(context);
+    if (location == null) {
+      return false;
+    }
+    sourceElement.set(location.psiLocation);
+    configuration.setTarget(location.label);
+    BlazeCommandRunConfigurationCommonState handlerState =
+        configuration.getHandlerStateIfType(BlazeCommandRunConfigurationCommonState.class);
+    if (handlerState == null) {
+      return false;
+    }
+    handlerState.getCommandState().setCommand(BlazeCommandName.TEST);
+
+    // remove old test filter flag if present
+    List<String> flags = new ArrayList<>(handlerState.getBlazeFlagsState().getRawFlags());
+    flags.removeIf((flag) -> flag.startsWith(BlazeFlags.TEST_FILTER));
+    if (location.testFilter != null) {
+      flags.add(location.testFilter);
+    }
+    handlerState.getBlazeFlagsState().setRawFlags(flags);
+
+    if (location.description != null) {
+      BlazeConfigurationNameBuilder nameBuilder = new BlazeConfigurationNameBuilder(configuration);
+      nameBuilder.setTargetString(location.description);
+      configuration.setName(nameBuilder.build());
+      configuration.setNameChangedByUser(true); // don't revert to generated name
+    } else {
+      configuration.setGeneratedName();
+    }
+    return true;
+  }
+
+  @Override
+  protected boolean doIsConfigFromContext(
+      BlazeCommandRunConfiguration configuration, ConfigurationContext context) {
+
+    TestLocation location = getTestLocation(context);
+    if (location == null) {
+      return false;
+    }
+    BlazeCommandRunConfigurationCommonState handlerState =
+        configuration.getHandlerStateIfType(BlazeCommandRunConfigurationCommonState.class);
+    if (handlerState == null) {
+      return false;
+    }
+    return BlazeCommandName.TEST.equals(handlerState.getCommandState().getCommand())
+        && location.label.equals(configuration.getTarget())
+        && Objects.equals(location.testFilter, handlerState.getTestFilterFlag());
+  }
+
+  @Nullable
+  private static TestLocation getTestLocation(ConfigurationContext context) {
+    if (!SmRunnerUtils.getSelectedSmRunnerTreeElements(context).isEmpty()) {
+      // handled by a different producer
+      return null;
+    }
+    PsiElement location = context.getPsiLocation();
+    if (location instanceof PsiDirectory) {
+      PsiDirectory dir = (PsiDirectory) location;
+      TargetIdeInfo target = getTestTargetIfUnique(dir);
+      return target != null ? TestLocation.fromDirectory(target.key.label, dir) : null;
+    }
+    Set<PsiClass> testClasses = selectedTestClasses(context);
+    if (testClasses.size() < 2) {
+      return null;
+    }
+    TargetIdeInfo target = getTestTargetIfUnique(testClasses);
+    if (target == null) {
+      return null;
+    }
+    testClasses = ProducerUtils.includeInnerTestClasses(testClasses);
+    return TestLocation.fromClasses(target.key.label, testClasses);
+  }
+
+  private static Set<PsiClass> selectedTestClasses(ConfigurationContext context) {
+    DataContext dataContext = context.getDataContext();
+    PsiElement[] elements = LangDataKeys.PSI_ELEMENT_ARRAY.getData(dataContext);
+    if (elements == null) {
+      return ImmutableSet.of();
+    }
+    return Arrays.stream(elements)
+        .map(JUnitUtil::getTestClass)
+        .filter(Objects::nonNull)
+        .filter(testClass -> !testClass.hasModifierProperty(PsiModifier.ABSTRACT))
+        .collect(Collectors.toSet());
+  }
+
+  @Nullable
+  private static TargetIdeInfo getTestTargetIfUnique(PsiDirectory directory) {
+    if (BlazePackage.hasBlazePackageChild(directory)) {
+      return null;
+    }
+    Set<PsiClass> classes = new HashSet<>();
+    addClassesInDirectory(directory, classes);
+    return getTestTargetIfUnique(classes);
+  }
+
+  private static void addClassesInDirectory(PsiDirectory directory, Set<PsiClass> list) {
+    Collections.addAll(list, JavaDirectoryService.getInstance().getClasses(directory));
+    for (PsiDirectory child : directory.getSubdirectories()) {
+      addClassesInDirectory(child, list);
+    }
+  }
+
+  @Nullable
+  private static TargetIdeInfo getTestTargetIfUnique(Set<PsiClass> classes) {
+    TargetIdeInfo testTarget = null;
+    for (PsiClass psiClass : classes) {
+      TargetIdeInfo target = testTargetForClass(psiClass);
+      if (target == null) {
+        continue;
+      }
+      if (testTarget != null && testTarget != target) {
+        return null;
+      }
+      testTarget = target;
+    }
+    return testTarget;
+  }
+
+  @Nullable
+  private static TargetIdeInfo testTargetForClass(PsiClass psiClass) {
+    PsiClass testClass = JUnitUtil.getTestClass(psiClass);
+    if (testClass == null || testClass.hasModifierProperty(PsiModifier.ABSTRACT)) {
+      return null;
+    }
+    TestIdeInfo.TestSize testSize = TestSizeAnnotationMap.getTestSize(psiClass);
+    return RunUtil.targetForTestClass(psiClass, testSize);
+  }
+
+  private static class TestLocation {
+    @Nullable
+    static TestLocation fromClasses(Label label, Set<PsiClass> classes) {
+      Map<PsiClass, Collection<Location<?>>> methodsPerClass =
+          classes.stream().collect(Collectors.toMap(c -> c, c -> ImmutableList.of()));
+      String filter = BlazeJUnitTestFilterFlags.testFilterForClassesAndMethods(methodsPerClass);
+      if (filter == null) {
+        return null;
+      }
+      PsiClass sampleClass =
+          classes.stream().min(Comparator.comparing(PsiClass::getName)).orElse(null);
+      String name = sampleClass.getName();
+      if (classes.size() > 1) {
+        name += String.format(" and %s others", classes.size() - 1);
+      }
+      return new TestLocation(label, sampleClass, filter, name);
+    }
+
+    @Nullable
+    static TestLocation fromDirectory(Label label, PsiDirectory dir) {
+      String packagePrefix =
+          ProjectFileIndex.SERVICE
+              .getInstance(dir.getProject())
+              .getPackageNameByDirectory(dir.getVirtualFile());
+      if (packagePrefix == null) {
+        return null;
+      }
+      String description =
+          packagePrefix.isEmpty() ? null : String.format("all in directory '%s'", dir.getName());
+      return new TestLocation(label, dir, packagePrefix, description);
+    }
+
+    private final Label label;
+    private final PsiElement psiLocation;
+    @Nullable private final String testFilter;
+    @Nullable private final String description;
+
+    private TestLocation(
+        Label label, PsiElement psiLocation, String testFilter, @Nullable String description) {
+      this.label = label;
+      this.psiLocation = psiLocation;
+      this.testFilter = !testFilter.isEmpty() ? BlazeFlags.TEST_FILTER + "=" + testFilter : null;
+      this.description = description;
+    }
+  }
+}
diff --git a/java/src/com/google/idea/blaze/java/run/producers/NonBlazeProducerSuppressor.java b/java/src/com/google/idea/blaze/java/run/producers/NonBlazeProducerSuppressor.java
index faf3545..4260daa 100644
--- a/java/src/com/google/idea/blaze/java/run/producers/NonBlazeProducerSuppressor.java
+++ b/java/src/com/google/idea/blaze/java/run/producers/NonBlazeProducerSuppressor.java
@@ -35,17 +35,18 @@
   private static final Collection<Class<? extends RunConfigurationProducer<?>>>
       PRODUCERS_TO_SUPPRESS =
           ImmutableList.of(
-              // JUnit test producers
               com.intellij.execution.junit.AllInDirectoryConfigurationProducer.class,
               com.intellij.execution.junit.AllInPackageConfigurationProducer.class,
               com.intellij.execution.junit.TestClassConfigurationProducer.class,
               com.intellij.execution.junit.TestMethodConfigurationProducer.class,
-              com.intellij.execution.junit.PatternConfigurationProducer.class);
+              com.intellij.execution.junit.PatternConfigurationProducer.class,
+              com.intellij.execution.application.ApplicationConfigurationProducer.class);
 
-  private static final ImmutableList<String> KOTLIN_JUNIT_PRODUCERS =
+  private static final ImmutableList<String> KOTLIN_PRODUCERS =
       ImmutableList.of(
           "org.jetbrains.kotlin.idea.run.KotlinJUnitRunConfigurationProducer",
-          "org.jetbrains.kotlin.idea.run.KotlinPatternConfigurationProducer");
+          "org.jetbrains.kotlin.idea.run.KotlinPatternConfigurationProducer",
+          "org.jetbrains.kotlin.idea.run.KotlinRunConfigurationProducer");
 
   private static Collection<Class<? extends RunConfigurationProducer<?>>> getKotlinProducers() {
     // rather than compiling against the Kotlin plugin, and including a switch in the our
@@ -55,7 +56,7 @@
       return ImmutableList.of();
     }
     ClassLoader loader = plugin.getPluginClassLoader();
-    return KOTLIN_JUNIT_PRODUCERS
+    return KOTLIN_PRODUCERS
         .stream()
         .map((qualifiedName) -> loadClass(loader, qualifiedName))
         .filter(Objects::nonNull)
@@ -71,7 +72,7 @@
         return (Class<RunConfigurationProducer<?>>) clazz;
       }
       return null;
-    } catch (ClassNotFoundException ignored) {
+    } catch (ClassNotFoundException | NoClassDefFoundError ignored) {
       return null;
     }
   }
diff --git a/java/src/com/google/idea/blaze/java/run/producers/ProducerUtils.java b/java/src/com/google/idea/blaze/java/run/producers/ProducerUtils.java
index b1b40ab..5690c02 100644
--- a/java/src/com/google/idea/blaze/java/run/producers/ProducerUtils.java
+++ b/java/src/com/google/idea/blaze/java/run/producers/ProducerUtils.java
@@ -21,17 +21,15 @@
 import com.intellij.execution.junit2.info.MethodLocation;
 import com.intellij.psi.PsiClass;
 import com.intellij.psi.PsiMethod;
+import java.util.Arrays;
+import java.util.HashSet;
 import java.util.Iterator;
+import java.util.Set;
+import java.util.stream.Collectors;
+import javax.annotation.Nullable;
 import org.jetbrains.annotations.NotNull;
-import org.jetbrains.annotations.Nullable;
 
-/**
- * Copy of {@link org.jetbrains.plugins.gradle.execution.test.runner.TestRunnerUtils}.
- *
- * <p>
- *
- * <p>Do not modify.
- */
+/** Utility methods for java test run configuration producers. */
 public class ProducerUtils {
   @Nullable
   public static Location<PsiMethod> getMethodLocation(@NotNull Location contextLocation) {
@@ -63,4 +61,19 @@
     }
     return null;
   }
+
+  /** For any test classes with nested inner test classes, also add the inner classes to the set. */
+  static Set<PsiClass> includeInnerTestClasses(Set<PsiClass> testClasses) {
+    Set<PsiClass> result = new HashSet<>(testClasses);
+    for (PsiClass psiClass : testClasses) {
+      result.addAll(getInnerTestClasses(psiClass));
+    }
+    return result;
+  }
+
+  static Set<PsiClass> getInnerTestClasses(PsiClass psiClass) {
+    return Arrays.stream(psiClass.getInnerClasses())
+        .filter(JUnitUtil::isTestClass)
+        .collect(Collectors.toSet());
+  }
 }
diff --git a/java/src/com/google/idea/blaze/java/run/producers/SubclassTestChooser.java b/java/src/com/google/idea/blaze/java/run/producers/SubclassTestChooser.java
new file mode 100644
index 0000000..1bc5250
--- /dev/null
+++ b/java/src/com/google/idea/blaze/java/run/producers/SubclassTestChooser.java
@@ -0,0 +1,73 @@
+/*
+ * Copyright 2017 The Bazel Authors. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.idea.blaze.java.run.producers;
+
+import com.intellij.execution.actions.ConfigurationContext;
+import com.intellij.execution.junit.JUnitUtil;
+import com.intellij.ide.util.PsiClassListCellRenderer;
+import com.intellij.openapi.ui.popup.JBPopupFactory;
+import com.intellij.psi.PsiClass;
+import com.intellij.psi.search.searches.ClassInheritorsSearch;
+import com.intellij.ui.components.JBList;
+import java.util.List;
+import java.util.function.Consumer;
+import java.util.stream.Collectors;
+import javax.swing.ListSelectionModel;
+
+/**
+ * Pop up a dialog to choose a child test class. Called when creating a run configuration from an
+ * abstract test class/method.
+ */
+public class SubclassTestChooser {
+
+  static void chooseSubclass(
+      ConfigurationContext context,
+      PsiClass abstractClass,
+      Consumer<PsiClass> callbackOnClassSelection) {
+    List<PsiClass> classes = findTestSubclasses(abstractClass);
+    if (classes.isEmpty()) {
+      return;
+    }
+    if (classes.size() == 1) {
+      callbackOnClassSelection.accept(classes.get(0));
+      return;
+    }
+    PsiClassListCellRenderer renderer = new PsiClassListCellRenderer();
+    classes.sort(renderer.getComparator());
+    // JBList has no generics in AS 2.2. TODO: Add generics here when we migrate to AS 2.3.
+    JBList list = new JBList(classes);
+    list.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
+    list.setCellRenderer(renderer);
+    JBPopupFactory.getInstance()
+        .createListPopupBuilder(list)
+        .setTitle("Choose test class to run")
+        .setMovable(false)
+        .setResizable(false)
+        .setRequestFocus(true)
+        .setItemChoosenCallback(
+            () -> callbackOnClassSelection.accept((PsiClass) list.getSelectedValue()))
+        .createPopup()
+        .showInBestPositionFor(context.getDataContext());
+  }
+
+  static List<PsiClass> findTestSubclasses(PsiClass abstractClass) {
+    return ClassInheritorsSearch.search(abstractClass)
+        .findAll()
+        .stream()
+        .filter(JUnitUtil::isTestClass)
+        .collect(Collectors.toList());
+  }
+}
diff --git a/java/src/com/google/idea/blaze/java/run/producers/TestMethodSelectionUtil.java b/java/src/com/google/idea/blaze/java/run/producers/TestMethodSelectionUtil.java
index e19f9bf..3db22aa 100644
--- a/java/src/com/google/idea/blaze/java/run/producers/TestMethodSelectionUtil.java
+++ b/java/src/com/google/idea/blaze/java/run/producers/TestMethodSelectionUtil.java
@@ -27,8 +27,8 @@
 import java.util.Collections;
 import java.util.Iterator;
 import java.util.List;
+import javax.annotation.Nullable;
 import org.jetbrains.annotations.NotNull;
-import org.jetbrains.annotations.Nullable;
 
 /** Helper functions for getting selected test methods. */
 public class TestMethodSelectionUtil {
diff --git a/java/src/com/google/idea/blaze/java/run/producers/TestSizeAnnotationMap.java b/java/src/com/google/idea/blaze/java/run/producers/TestSizeAnnotationMap.java
index 13062b3..66cb44f 100644
--- a/java/src/com/google/idea/blaze/java/run/producers/TestSizeAnnotationMap.java
+++ b/java/src/com/google/idea/blaze/java/run/producers/TestSizeAnnotationMap.java
@@ -25,7 +25,7 @@
 
 /** Maps method and class annotations to our test size enumeration. */
 public class TestSizeAnnotationMap {
-  private static ImmutableMap<String, TestIdeInfo.TestSize> ANNOTATION_TO_TEST_SIZE =
+  private static final ImmutableMap<String, TestIdeInfo.TestSize> ANNOTATION_TO_TEST_SIZE =
       ImmutableMap.<String, TestIdeInfo.TestSize>builder()
           .put("com.google.testing.testsize.SmallTest", TestIdeInfo.TestSize.SMALL)
           .put("com.google.testing.testsize.MediumTest", TestIdeInfo.TestSize.MEDIUM)
diff --git a/java/src/com/google/idea/blaze/java/sync/BlazeJavaSyncAugmenter.java b/java/src/com/google/idea/blaze/java/sync/BlazeJavaSyncAugmenter.java
index 7c4aa21..0fbe869 100644
--- a/java/src/com/google/idea/blaze/java/sync/BlazeJavaSyncAugmenter.java
+++ b/java/src/com/google/idea/blaze/java/sync/BlazeJavaSyncAugmenter.java
@@ -25,7 +25,7 @@
 /** Augments the java importer */
 public interface BlazeJavaSyncAugmenter {
   ExtensionPointName<BlazeJavaSyncAugmenter> EP_NAME =
-      ExtensionPointName.create("com.google.idea.blaze.java.JavaSyncAugmenter");
+      ExtensionPointName.create("com.google.idea.blaze.JavaSyncAugmenter");
 
   /**
    * Adds extra libraries for this source rule.
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 b971b94..1d31d91 100644
--- a/java/src/com/google/idea/blaze/java/sync/BlazeJavaSyncPlugin.java
+++ b/java/src/com/google/idea/blaze/java/sync/BlazeJavaSyncPlugin.java
@@ -17,6 +17,7 @@
 
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableSet;
+import com.google.idea.blaze.base.command.info.BlazeInfo;
 import com.google.idea.blaze.base.ideinfo.ArtifactLocation;
 import com.google.idea.blaze.base.ideinfo.LibraryArtifact;
 import com.google.idea.blaze.base.ideinfo.TargetMap;
@@ -41,7 +42,6 @@
 import com.google.idea.blaze.base.sync.libraries.LibrarySource;
 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 com.google.idea.blaze.java.projectview.ExcludeLibrarySection;
@@ -109,7 +109,7 @@
       WorkspaceRoot workspaceRoot,
       ProjectViewSet projectViewSet,
       WorkspaceLanguageSettings workspaceLanguageSettings,
-      BlazeRoots blazeRoots,
+      BlazeInfo blazeInfo,
       @Nullable WorkingSet workingSet,
       WorkspacePathResolver workspacePathResolver,
       ArtifactLocationDecoder artifactLocationDecoder,
@@ -249,7 +249,8 @@
 
   @Nullable
   @Override
-  public LibrarySource getLibrarySource(BlazeProjectData blazeProjectData) {
+  public LibrarySource getLibrarySource(
+      ProjectViewSet projectViewSet, BlazeProjectData blazeProjectData) {
     if (!blazeProjectData.workspaceLanguageSettings.isLanguageActive(LanguageClass.JAVA)) {
       return null;
     }
diff --git a/java/src/com/google/idea/blaze/java/sync/DuplicateSourceDetector.java b/java/src/com/google/idea/blaze/java/sync/DuplicateSourceDetector.java
index 10db930..d03fc00 100644
--- a/java/src/com/google/idea/blaze/java/sync/DuplicateSourceDetector.java
+++ b/java/src/com/google/idea/blaze/java/sync/DuplicateSourceDetector.java
@@ -24,7 +24,7 @@
 import com.google.idea.blaze.base.scope.BlazeContext;
 import com.google.idea.blaze.base.scope.output.PerformanceWarning;
 import java.util.Collection;
-import java.util.Collections;
+import java.util.Comparator;
 import java.util.List;
 import java.util.Set;
 
@@ -64,12 +64,7 @@
       return;
     }
 
-    Collections.sort(
-        duplicates,
-        (lhs, rhs) ->
-            lhs.artifactLocation
-                .getRelativePath()
-                .compareTo(rhs.artifactLocation.getRelativePath()));
+    duplicates.sort(Comparator.comparing(lhs -> lhs.artifactLocation.getRelativePath()));
 
     context.output(new PerformanceWarning("Duplicate sources detected:"));
     for (Duplicate duplicate : duplicates) {
diff --git a/java/src/com/google/idea/blaze/java/sync/JavaPrefetchFileSource.java b/java/src/com/google/idea/blaze/java/sync/JavaPrefetchFileSource.java
index 7e3255f..c39eb64 100644
--- a/java/src/com/google/idea/blaze/java/sync/JavaPrefetchFileSource.java
+++ b/java/src/com/google/idea/blaze/java/sync/JavaPrefetchFileSource.java
@@ -19,6 +19,7 @@
 import com.google.idea.blaze.base.model.BlazeLibrary;
 import com.google.idea.blaze.base.model.BlazeProjectData;
 import com.google.idea.blaze.base.prefetch.PrefetchFileSource;
+import com.google.idea.blaze.base.projectview.ProjectViewSet;
 import com.google.idea.blaze.base.sync.libraries.BlazeLibraryCollector;
 import com.google.idea.blaze.base.sync.workspace.ArtifactLocationDecoder;
 import com.google.idea.blaze.java.libraries.JarCache;
@@ -35,7 +36,10 @@
 public class JavaPrefetchFileSource implements PrefetchFileSource {
   @Override
   public void addFilesToPrefetch(
-      Project project, BlazeProjectData blazeProjectData, Collection<File> files) {
+      Project project,
+      ProjectViewSet projectViewSet,
+      BlazeProjectData blazeProjectData,
+      Collection<File> files) {
     BlazeJavaSyncData syncData = blazeProjectData.syncState.get(BlazeJavaSyncData.class);
     if (syncData == null) {
       return;
@@ -47,7 +51,8 @@
     boolean attachSourcesByDefault =
         BlazeJavaUserSettings.getInstance().getAttachSourcesByDefault();
     SourceJarManager sourceJarManager = SourceJarManager.getInstance(project);
-    Collection<BlazeLibrary> libraries = BlazeLibraryCollector.getLibraries(blazeProjectData);
+    Collection<BlazeLibrary> libraries =
+        BlazeLibraryCollector.getLibraries(projectViewSet, blazeProjectData);
     ArtifactLocationDecoder artifactLocationDecoder = blazeProjectData.artifactLocationDecoder;
     for (BlazeLibrary library : libraries) {
       if (!(library instanceof BlazeJarLibrary)) {
diff --git a/java/src/com/google/idea/blaze/java/sync/importer/BlazeJavaWorkspaceImporter.java b/java/src/com/google/idea/blaze/java/sync/importer/BlazeJavaWorkspaceImporter.java
index 2e8df7f..c089e53 100644
--- a/java/src/com/google/idea/blaze/java/sync/importer/BlazeJavaWorkspaceImporter.java
+++ b/java/src/com/google/idea/blaze/java/sync/importer/BlazeJavaWorkspaceImporter.java
@@ -167,10 +167,7 @@
       List<LibraryArtifact> allJars = Lists.newArrayList();
       allJars.addAll(javaIdeInfo.jars);
       Collection<BlazeJarLibrary> libraries =
-          allJars
-              .stream()
-              .map(library -> new BlazeJarLibrary(library, target.key))
-              .collect(Collectors.toList());
+          allJars.stream().map(BlazeJarLibrary::new).collect(Collectors.toList());
 
       targetKeyToLibrary.putAll(target.key, libraries);
       for (BlazeJarLibrary library : libraries) {
@@ -189,7 +186,7 @@
               protoLibraryLegacyInfo.jarsV1,
               protoLibraryLegacyInfo.jarsMutable,
               protoLibraryLegacyInfo.jarsImmutable)) {
-        addLibraryToJdeps(jdepsPathToLibrary, new BlazeJarLibrary(libraryArtifact, target.key));
+        addLibraryToJdeps(jdepsPathToLibrary, new BlazeJarLibrary(libraryArtifact));
       }
     }
 
@@ -303,7 +300,7 @@
 
       if (libraries != null) {
         for (LibraryArtifact libraryArtifact : libraries) {
-          BlazeJarLibrary library = new BlazeJarLibrary(libraryArtifact, targetKey);
+          BlazeJarLibrary library = new BlazeJarLibrary(libraryArtifact);
           result.put(library.key, library);
         }
       }
@@ -374,14 +371,10 @@
       }
     }
     workspaceBuilder.generatedJarsFromSourceTargets.addAll(
-        javaIdeInfo
-            .generatedJars
-            .stream()
-            .map(libraryArtifact -> new BlazeJarLibrary(libraryArtifact, targetKey))
-            .collect(Collectors.toList()));
+        javaIdeInfo.generatedJars.stream().map(BlazeJarLibrary::new).collect(Collectors.toList()));
     if (javaIdeInfo.filteredGenJar != null) {
       workspaceBuilder.generatedJarsFromSourceTargets.add(
-          new BlazeJarLibrary(javaIdeInfo.filteredGenJar, targetKey));
+          new BlazeJarLibrary(javaIdeInfo.filteredGenJar));
     }
 
     for (BlazeJavaSyncAugmenter augmenter : augmenters) {
diff --git a/java/src/com/google/idea/blaze/java/sync/importer/JavaSourceFilter.java b/java/src/com/google/idea/blaze/java/sync/importer/JavaSourceFilter.java
index 579eede..474b729 100644
--- a/java/src/com/google/idea/blaze/java/sync/importer/JavaSourceFilter.java
+++ b/java/src/com/google/idea/blaze/java/sync/importer/JavaSourceFilter.java
@@ -25,10 +25,12 @@
 import com.google.idea.blaze.base.model.primitives.WorkspaceRoot;
 import com.google.idea.blaze.base.projectview.ProjectViewSet;
 import com.google.idea.blaze.base.sync.projectview.ProjectViewTargetImportFilter;
+import com.google.idea.blaze.java.sync.source.JavaLikeLanguage;
 import com.intellij.openapi.project.Project;
 import java.util.Collection;
 import java.util.List;
 import java.util.Map;
+import java.util.function.Predicate;
 import java.util.stream.Collectors;
 
 /** Segments java rules into source/libraries */
@@ -59,14 +61,11 @@
             .collect(Collectors.toList());
 
     targetToJavaSources = Maps.newHashMap();
+    Predicate<ArtifactLocation> isSourceFile = JavaLikeLanguage.getSourceFileMatcher();
     for (TargetIdeInfo target : javaTargets) {
-      List<ArtifactLocation> javaSources =
-          target
-              .sources
-              .stream()
-              .filter(source -> source.getRelativePath().endsWith(".java"))
-              .collect(Collectors.toList());
-      targetToJavaSources.put(target.key, javaSources);
+      List<ArtifactLocation> javaLikeSources =
+          target.sources.stream().filter(isSourceFile).collect(Collectors.toList());
+      targetToJavaSources.put(target.key, javaLikeSources);
     }
 
     sourceTargets = Lists.newArrayList();
diff --git a/java/src/com/google/idea/blaze/java/sync/jdeps/JdepsMap.java b/java/src/com/google/idea/blaze/java/sync/jdeps/JdepsMap.java
index ac53134..b58b8cc 100644
--- a/java/src/com/google/idea/blaze/java/sync/jdeps/JdepsMap.java
+++ b/java/src/com/google/idea/blaze/java/sync/jdeps/JdepsMap.java
@@ -17,7 +17,7 @@
 
 import com.google.idea.blaze.base.ideinfo.TargetKey;
 import java.util.List;
-import org.jetbrains.annotations.Nullable;
+import javax.annotation.Nullable;
 
 /** Map of rule -> jdeps dependencies. */
 public interface JdepsMap {
diff --git a/java/src/com/google/idea/blaze/java/sync/model/BlazeJarLibrary.java b/java/src/com/google/idea/blaze/java/sync/model/BlazeJarLibrary.java
index 3d0b128..d5ff52c 100644
--- a/java/src/com/google/idea/blaze/java/sync/model/BlazeJarLibrary.java
+++ b/java/src/com/google/idea/blaze/java/sync/model/BlazeJarLibrary.java
@@ -17,7 +17,6 @@
 
 import com.google.common.base.Objects;
 import com.google.idea.blaze.base.ideinfo.LibraryArtifact;
-import com.google.idea.blaze.base.ideinfo.TargetKey;
 import com.google.idea.blaze.base.model.BlazeLibrary;
 import com.google.idea.blaze.base.model.LibraryKey;
 import com.google.idea.blaze.base.sync.workspace.ArtifactLocationDecoder;
@@ -33,16 +32,13 @@
 /** An immutable reference to a .jar required by a rule. */
 @Immutable
 public final class BlazeJarLibrary extends BlazeLibrary {
-  private static final long serialVersionUID = 2L;
+  private static final long serialVersionUID = 3L;
 
   public final LibraryArtifact libraryArtifact;
 
-  public final TargetKey originatingTarget;
-
-  public BlazeJarLibrary(LibraryArtifact libraryArtifact, TargetKey originatingTarget) {
+  public BlazeJarLibrary(LibraryArtifact libraryArtifact) {
     super(LibraryKey.fromJarFile(libraryArtifact.jarForIntellijLibrary()));
     this.libraryArtifact = libraryArtifact;
-    this.originatingTarget = originatingTarget;
   }
 
   @Override
@@ -68,7 +64,7 @@
 
   @Override
   public int hashCode() {
-    return Objects.hashCode(super.hashCode(), libraryArtifact, originatingTarget);
+    return Objects.hashCode(super.hashCode(), libraryArtifact);
   }
 
   @Override
@@ -82,8 +78,6 @@
 
     BlazeJarLibrary that = (BlazeJarLibrary) other;
 
-    return super.equals(other)
-        && Objects.equal(libraryArtifact, that.libraryArtifact)
-        && Objects.equal(originatingTarget, that.originatingTarget);
+    return super.equals(other) && Objects.equal(libraryArtifact, that.libraryArtifact);
   }
 }
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 3f7b512..acbf755 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
@@ -28,6 +28,8 @@
 import com.intellij.openapi.util.io.FileUtil;
 import com.intellij.openapi.vfs.VirtualFileManager;
 import java.io.File;
+import java.util.HashMap;
+import java.util.Map;
 import javax.annotation.Nullable;
 import org.jetbrains.jps.model.JpsElement;
 import org.jetbrains.jps.model.java.JavaResourceRootType;
@@ -57,16 +59,20 @@
 
   @Override
   public ImmutableMap<File, SourceFolder> initializeSourceFolders(ContentEntry contentEntry) {
-    ImmutableMap.Builder<File, SourceFolder> output = ImmutableMap.builder();
+    Map<File, SourceFolder> map = new HashMap<>();
     BlazeContentEntry javaContentEntry =
         blazeContentEntries.get(UrlUtil.urlToFile(contentEntry.getUrl()));
     if (javaContentEntry != null) {
       for (BlazeSourceDirectory sourceDirectory : javaContentEntry.sources) {
+        File file = sourceDirectory.getDirectory();
+        if (map.containsKey(file)) {
+          continue;
+        }
         SourceFolder sourceFolder = addSourceFolderToContentEntry(contentEntry, sourceDirectory);
-        output.put(UrlUtil.urlToFile(sourceFolder.getUrl()), sourceFolder);
+        map.put(file, sourceFolder);
       }
     }
-    return output.build();
+    return ImmutableMap.copyOf(map);
   }
 
   @Override
@@ -99,8 +105,10 @@
     if (Strings.isNullOrEmpty(relativePath)) {
       return parentPackagePrefix;
     }
-
-    return parentPackagePrefix + "." + relativePath.replaceAll(File.separator, ".");
+    relativePath = relativePath.replaceAll(File.separator, ".");
+    return Strings.isNullOrEmpty(parentPackagePrefix)
+        ? relativePath
+        : parentPackagePrefix + "." + relativePath;
   }
 
   @VisibleForTesting
diff --git a/java/src/com/google/idea/blaze/java/sync/projectstructure/Jdks.java b/java/src/com/google/idea/blaze/java/sync/projectstructure/Jdks.java
index e80c98b..704f4c4 100644
--- a/java/src/com/google/idea/blaze/java/sync/projectstructure/Jdks.java
+++ b/java/src/com/google/idea/blaze/java/sync/projectstructure/Jdks.java
@@ -36,9 +36,9 @@
 import java.util.Collection;
 import java.util.Iterator;
 import java.util.List;
+import javax.annotation.Nullable;
 import org.jetbrains.annotations.NonNls;
 import org.jetbrains.annotations.NotNull;
-import org.jetbrains.annotations.Nullable;
 
 /** Utility methods related to IDEA JDKs. */
 public class Jdks {
diff --git a/java/src/com/google/idea/blaze/java/sync/source/JavaLikeLanguage.java b/java/src/com/google/idea/blaze/java/sync/source/JavaLikeLanguage.java
new file mode 100644
index 0000000..f6c791a
--- /dev/null
+++ b/java/src/com/google/idea/blaze/java/sync/source/JavaLikeLanguage.java
@@ -0,0 +1,56 @@
+/*
+ * Copyright 2017 The Bazel Authors. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.idea.blaze.java.sync.source;
+
+import com.google.common.collect.ImmutableSet;
+import com.google.idea.blaze.base.ideinfo.ArtifactLocation;
+import com.intellij.openapi.extensions.ExtensionPointName;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Set;
+import java.util.function.Predicate;
+import java.util.stream.Collectors;
+
+/**
+ * For languages similar to Java to reuse certain parts of the Java plugin. E.g., package prefix
+ * calculation.
+ */
+public interface JavaLikeLanguage {
+  ExtensionPointName<JavaLikeLanguage> EP_NAME =
+      ExtensionPointName.create("com.google.idea.blaze.JavaLikeLanguage");
+
+  static Predicate<ArtifactLocation> getSourceFileMatcher() {
+    final Set<String> fileExtensions =
+        Arrays.stream(EP_NAME.getExtensions())
+            .map(JavaLikeLanguage::getFileExtensions)
+            .flatMap(Collection::stream)
+            .collect(Collectors.toSet());
+    return artifactLocation ->
+        fileExtensions
+            .stream()
+            .anyMatch(extension -> artifactLocation.getRelativePath().endsWith(extension));
+  }
+
+  Set<String> getFileExtensions();
+
+  /** Java is itself a Java-like language. */
+  class Java implements JavaLikeLanguage {
+    @Override
+    public Set<String> getFileExtensions() {
+      return ImmutableSet.of(".java");
+    }
+  }
+}
diff --git a/java/src/com/google/idea/blaze/java/sync/source/JavaSourcePackageReader.java b/java/src/com/google/idea/blaze/java/sync/source/JavaSourcePackageReader.java
index 64e1cb5..a29b5bc 100644
--- a/java/src/com/google/idea/blaze/java/sync/source/JavaSourcePackageReader.java
+++ b/java/src/com/google/idea/blaze/java/sync/source/JavaSourcePackageReader.java
@@ -42,8 +42,8 @@
 
   private static final Logger logger = Logger.getInstance(SourceDirectoryCalculator.class);
 
-  private static final Pattern JAVA_PACKAGE_PATTERN =
-      Pattern.compile("^\\s*package\\s+([\\w\\.]+);");
+  // Package declaration of java-like languages.
+  private static final Pattern PACKAGE_PATTERN = Pattern.compile("^\\s*package\\s+([\\w\\.]+)");
 
   @Override
   @Nullable
@@ -61,7 +61,7 @@
       String javaLine;
 
       while ((javaLine = javaReader.readLine()) != null) {
-        Matcher packageMatch = JAVA_PACKAGE_PATTERN.matcher(javaLine);
+        Matcher packageMatch = PACKAGE_PATTERN.matcher(javaLine);
         if (packageMatch.find()) {
           return packageMatch.group(1);
         }
diff --git a/java/src/com/google/idea/blaze/java/sync/source/PackageManifestReader.java b/java/src/com/google/idea/blaze/java/sync/source/PackageManifestReader.java
index 52fced0..cadef54 100644
--- a/java/src/com/google/idea/blaze/java/sync/source/PackageManifestReader.java
+++ b/java/src/com/google/idea/blaze/java/sync/source/PackageManifestReader.java
@@ -15,6 +15,7 @@
  */
 package com.google.idea.blaze.java.sync.source;
 
+import com.google.common.base.Joiner;
 import com.google.common.collect.ImmutableMap;
 import com.google.common.collect.Lists;
 import com.google.common.collect.Maps;
@@ -29,11 +30,13 @@
 import com.google.idea.blaze.base.prefetch.PrefetchService;
 import com.google.idea.blaze.base.scope.BlazeContext;
 import com.google.idea.blaze.base.sync.workspace.ArtifactLocationDecoder;
+import com.google.repackaged.devtools.build.lib.ideinfo.androidstudio.PackageManifestOuterClass;
 import com.google.repackaged.devtools.build.lib.ideinfo.androidstudio.PackageManifestOuterClass.JavaSourcePackage;
 import com.google.repackaged.devtools.build.lib.ideinfo.androidstudio.PackageManifestOuterClass.PackageManifest;
 import com.intellij.openapi.components.ServiceManager;
 import com.intellij.openapi.diagnostic.Logger;
 import com.intellij.openapi.project.Project;
+import com.intellij.openapi.util.text.StringUtil;
 import java.io.BufferedInputStream;
 import java.io.File;
 import java.io.IOException;
@@ -111,7 +114,7 @@
     return manifestMap;
   }
 
-  protected Map<ArtifactLocation, String> parseManifestFile(File packageManifest) {
+  private static Map<ArtifactLocation, String> parseManifestFile(File packageManifest) {
     Map<ArtifactLocation, String> outputMap = Maps.newHashMap();
     InputStreamProvider inputStreamProvider = InputStreamProvider.getInstance();
 
@@ -119,14 +122,7 @@
       try (BufferedInputStream bufferedInputStream = new BufferedInputStream(input)) {
         PackageManifest proto = PackageManifest.parseFrom(bufferedInputStream);
         for (JavaSourcePackage source : proto.getSourcesList()) {
-          ArtifactLocation artifactLocation =
-              ArtifactLocation.builder()
-                  .setRootExecutionPathFragment(
-                      source.getArtifactLocation().getRootExecutionPathFragment())
-                  .setRelativePath(source.getArtifactLocation().getRelativePath())
-                  .setIsSource(source.getArtifactLocation().getIsSource())
-                  .build();
-          outputMap.put(artifactLocation, source.getPackageString());
+          outputMap.put(fromProto(source.getArtifactLocation()), source.getPackageString());
         }
       }
       return outputMap;
@@ -135,4 +131,26 @@
       return outputMap;
     }
   }
+
+  private static ArtifactLocation fromProto(PackageManifestOuterClass.ArtifactLocation location) {
+    String relativePath = location.getRelativePath();
+    String rootExecutionPathFragment = location.getRootExecutionPathFragment();
+    if (!location.getIsNewExternalVersion() && location.getIsExternal()) {
+      // fix up incorrect paths created with older aspect version
+      // Note: bazel always uses the '/' separator here, even on windows.
+      List<String> components = StringUtil.split(relativePath, "/");
+      if (components.size() > 2) {
+        relativePath = Joiner.on('/').join(components.subList(2, components.size()));
+        String prefix = components.get(0) + "/" + components.get(1);
+        rootExecutionPathFragment =
+            rootExecutionPathFragment.isEmpty() ? prefix : rootExecutionPathFragment + "/" + prefix;
+      }
+    }
+    return ArtifactLocation.builder()
+        .setRootExecutionPathFragment(rootExecutionPathFragment)
+        .setRelativePath(relativePath)
+        .setIsSource(location.getIsSource())
+        .setIsExternal(location.getIsExternal())
+        .build();
+  }
 }
diff --git a/java/src/com/google/idea/blaze/java/sync/source/SourceDirectoryCalculator.java b/java/src/com/google/idea/blaze/java/sync/source/SourceDirectoryCalculator.java
index e1e4203..14afec5 100644
--- a/java/src/com/google/idea/blaze/java/sync/source/SourceDirectoryCalculator.java
+++ b/java/src/com/google/idea/blaze/java/sync/source/SourceDirectoryCalculator.java
@@ -50,10 +50,12 @@
 import java.io.File;
 import java.util.Collection;
 import java.util.Collections;
+import java.util.Comparator;
 import java.util.List;
 import java.util.Map;
 import java.util.Set;
 import java.util.concurrent.ExecutionException;
+import java.util.function.Predicate;
 import java.util.stream.Collectors;
 import javax.annotation.Nullable;
 
@@ -72,7 +74,7 @@
 
   private static final JavaPackageReader generatedFileJavaPackageReader =
       new FilePathJavaPackageReader();
-  private final ListeningExecutorService executorService = MoreExecutors.sameThreadExecutor();
+  private final ListeningExecutorService executorService = MoreExecutors.newDirectExecutorService();
   private final ListeningExecutorService packageReaderExecutorService =
       MoreExecutors.listeningDecorator(new TransientExecutor(16));
 
@@ -132,7 +134,7 @@
               result.add(new BlazeContentEntry(contentRoot, sourceDirectories));
             }
           }
-          Collections.sort(result, (lhs, rhs) -> lhs.contentRoot.compareTo(rhs.contentRoot));
+          result.sort(Comparator.comparing(lhs -> lhs.contentRoot));
         });
     return ImmutableList.copyOf(result);
   }
@@ -197,10 +199,11 @@
       Collection<SourceArtifact> sourceArtifacts,
       Collection<JavaPackageReader> javaPackageReaders) {
 
-    // Split out java files
+    // Split out java-like files
+    Predicate<ArtifactLocation> isSourceFile = JavaLikeLanguage.getSourceFileMatcher();
     List<SourceArtifact> javaArtifacts = Lists.newArrayList();
     for (SourceArtifact sourceArtifact : sourceArtifacts) {
-      if (isJavaFile(sourceArtifact.artifactLocation)) {
+      if (isSourceFile.test(sourceArtifact.artifactLocation)) {
         javaArtifacts.add(sourceArtifact);
       }
     }
@@ -518,8 +521,4 @@
       this.directoryName = directoryName;
     }
   }
-
-  private static boolean isJavaFile(ArtifactLocation artifactLocation) {
-    return artifactLocation.getRelativePath().endsWith(".java");
-  }
 }
diff --git a/java/src/com/google/idea/blaze/java/syncstatus/BlazeJavaSyncStatusClassNodeDecorator.java b/java/src/com/google/idea/blaze/java/syncstatus/BlazeJavaSyncStatusClassNodeDecorator.java
index 01d7f40..6ffabc2 100644
--- a/java/src/com/google/idea/blaze/java/syncstatus/BlazeJavaSyncStatusClassNodeDecorator.java
+++ b/java/src/com/google/idea/blaze/java/syncstatus/BlazeJavaSyncStatusClassNodeDecorator.java
@@ -48,7 +48,7 @@
     }
 
     Project project = node.getProject();
-    if (SyncStatusHelper.getInstance(project).isUnsynced(virtualFile)) {
+    if (SyncStatusHelper.isUnsynced(project, virtualFile)) {
       data.clearText();
       data.addText(psiClass.getName(), SimpleTextAttributes.GRAY_ATTRIBUTES);
       data.addText(" (unsynced)", SimpleTextAttributes.GRAY_ATTRIBUTES);
diff --git a/java/src/com/google/idea/blaze/java/syncstatus/BlazeJavaSyncStatusEditorTabColorProvider.java b/java/src/com/google/idea/blaze/java/syncstatus/BlazeJavaSyncStatusEditorTabColorProvider.java
index 91234b1..26b97fa 100644
--- a/java/src/com/google/idea/blaze/java/syncstatus/BlazeJavaSyncStatusEditorTabColorProvider.java
+++ b/java/src/com/google/idea/blaze/java/syncstatus/BlazeJavaSyncStatusEditorTabColorProvider.java
@@ -20,8 +20,8 @@
 import com.intellij.openapi.vfs.VirtualFile;
 import com.intellij.ui.JBColor;
 import java.awt.Color;
+import javax.annotation.Nullable;
 import org.jetbrains.annotations.NotNull;
-import org.jetbrains.annotations.Nullable;
 
 /** Changes the color for unsynced files. */
 public class BlazeJavaSyncStatusEditorTabColorProvider implements EditorTabColorProvider {
@@ -31,8 +31,7 @@
   @Nullable
   @Override
   public Color getEditorTabColor(@NotNull Project project, @NotNull VirtualFile file) {
-    if (file.getName().endsWith(".java")
-        && SyncStatusHelper.getInstance(project).isUnsynced(file)) {
+    if (file.getName().endsWith(".java") && SyncStatusHelper.isUnsynced(project, file)) {
       return UNSYNCED_COLOR;
     }
     return null;
diff --git a/java/src/com/google/idea/blaze/java/syncstatus/BlazeJavaSyncStatusEditorTabTitleProvider.java b/java/src/com/google/idea/blaze/java/syncstatus/BlazeJavaSyncStatusEditorTabTitleProvider.java
index aa96de8..43af36c 100644
--- a/java/src/com/google/idea/blaze/java/syncstatus/BlazeJavaSyncStatusEditorTabTitleProvider.java
+++ b/java/src/com/google/idea/blaze/java/syncstatus/BlazeJavaSyncStatusEditorTabTitleProvider.java
@@ -18,14 +18,14 @@
 import com.intellij.openapi.fileEditor.impl.EditorTabTitleProvider;
 import com.intellij.openapi.project.Project;
 import com.intellij.openapi.vfs.VirtualFile;
-import org.jetbrains.annotations.Nullable;
+import javax.annotation.Nullable;
 
 /** Changes the tab title for unsynced files. */
 public class BlazeJavaSyncStatusEditorTabTitleProvider implements EditorTabTitleProvider {
   @Nullable
   @Override
   public String getEditorTabTitle(Project project, VirtualFile file) {
-    if (file.getName().endsWith("java") && SyncStatusHelper.getInstance(project).isUnsynced(file)) {
+    if (file.getName().endsWith("java") && SyncStatusHelper.isUnsynced(project, file)) {
       return file.getPresentableName() + " (unsynced)";
     }
     return null;
diff --git a/java/src/com/google/idea/blaze/java/syncstatus/SyncStatusHelper.java b/java/src/com/google/idea/blaze/java/syncstatus/SyncStatusHelper.java
index ff6365c..c79888d 100644
--- a/java/src/com/google/idea/blaze/java/syncstatus/SyncStatusHelper.java
+++ b/java/src/com/google/idea/blaze/java/syncstatus/SyncStatusHelper.java
@@ -17,15 +17,9 @@
 
 import com.google.common.collect.ImmutableSet;
 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.google.idea.blaze.base.sync.SyncCache;
 import com.google.idea.blaze.base.sync.workspace.ArtifactLocationDecoder;
 import com.google.idea.blaze.java.sync.model.BlazeJavaSyncData;
-import com.intellij.openapi.components.ServiceManager;
 import com.intellij.openapi.project.Project;
 import com.intellij.openapi.roots.ProjectFileIndex;
 import com.intellij.openapi.vfs.VirtualFile;
@@ -34,27 +28,16 @@
 
 class SyncStatusHelper {
 
-  static SyncStatusHelper getInstance(Project project) {
-    return ServiceManager.getService(project, SyncStatusHelper.class);
-  }
-
-  private final Project project;
-  private Set<File> syncedJavaFiles = null;
-
-  SyncStatusHelper(Project project) {
-    this.project = project;
-  }
-
-  boolean isUnsynced(VirtualFile virtualFile) {
+  static boolean isUnsynced(Project project, VirtualFile virtualFile) {
     if (!virtualFile.isInLocalFileSystem()) {
       return false;
     }
     if (ProjectFileIndex.SERVICE.getInstance(project).getModuleForFile(virtualFile) == null) {
       return false;
     }
-    if (syncedJavaFiles == null) {
-      syncedJavaFiles = refresh();
-    }
+    Set<File> syncedJavaFiles =
+        SyncCache.getInstance(project)
+            .get(SyncStatusHelper.class, SyncStatusHelper::getSyncedJavaFiles);
     if (syncedJavaFiles == null) {
       return false;
     }
@@ -62,33 +45,14 @@
     return !syncedJavaFiles.contains(file);
   }
 
-  Set<File> refresh() {
-    BlazeProjectData blazeProjectData =
-        BlazeProjectDataManager.getInstance(project).getBlazeProjectData();
-    if (blazeProjectData == null) {
-      return null;
-    }
-    BlazeJavaSyncData syncData = blazeProjectData.syncState.get(BlazeJavaSyncData.class);
+  @SuppressWarnings("unused")
+  private static Set<File> getSyncedJavaFiles(Project project, BlazeProjectData projectData) {
+    BlazeJavaSyncData syncData = projectData.syncState.get(BlazeJavaSyncData.class);
     if (syncData == null) {
-      return null;
+      return ImmutableSet.of();
     }
-    ArtifactLocationDecoder artifactLocationDecoder = blazeProjectData.artifactLocationDecoder;
-    return ImmutableSet.<File>builder()
-        .addAll(artifactLocationDecoder.decodeAll(syncData.importResult.javaSourceFiles))
-        .build();
-  }
-
-  static class UpdateSyncStatusMap extends SyncListener.Adapter {
-    @Override
-    public void onSyncComplete(
-        Project project,
-        BlazeContext context,
-        BlazeImportSettings importSettings,
-        ProjectViewSet projectViewSet,
-        BlazeProjectData blazeProjectData,
-        SyncMode syncMode,
-        SyncResult syncResult) {
-      getInstance(project).syncedJavaFiles = null;
-    }
+    ArtifactLocationDecoder artifactLocationDecoder = projectData.artifactLocationDecoder;
+    return ImmutableSet.copyOf(
+        artifactLocationDecoder.decodeAll(syncData.importResult.javaSourceFiles));
   }
 }
diff --git a/java/src/com/google/idea/blaze/java/ui/BlazeIntelliJProblemsView.java b/java/src/com/google/idea/blaze/java/ui/BlazeIntelliJProblemsView.java
index a2f0582..2c58b33 100644
--- a/java/src/com/google/idea/blaze/java/ui/BlazeIntelliJProblemsView.java
+++ b/java/src/com/google/idea/blaze/java/ui/BlazeIntelliJProblemsView.java
@@ -41,7 +41,7 @@
   public void addMessage(IssueOutput issue, UUID sessionId) {
     VirtualFile virtualFile =
         issue.getFile() != null
-            ? VfsUtil.findFileByIoFile(issue.getFile(), true /* refresh */)
+            ? VfsUtil.findFileByIoFile(issue.getFile(), /* refresh */ true)
             : null;
     CompilerMessageCategory category =
         issue.getCategory() == IssueOutput.Category.ERROR
diff --git a/java/src/com/google/idea/blaze/java/wizard2/BlazeEditProjectViewImportWizardStep.java b/java/src/com/google/idea/blaze/java/wizard2/BlazeEditProjectViewImportWizardStep.java
index 4a6bc6a..3d57300 100644
--- a/java/src/com/google/idea/blaze/java/wizard2/BlazeEditProjectViewImportWizardStep.java
+++ b/java/src/com/google/idea/blaze/java/wizard2/BlazeEditProjectViewImportWizardStep.java
@@ -83,6 +83,7 @@
   public void onWizardFinished() throws CommitStepException {
     try {
       getProjectBuilder().commit();
+      control.commit();
     } catch (BlazeProjectCommitException e) {
       throw new CommitStepException(e.getMessage());
     }
diff --git a/java/src/com/google/idea/blaze/java/wizard2/BlazeNewProjectWizard.java b/java/src/com/google/idea/blaze/java/wizard2/BlazeNewProjectWizard.java
index 62be22e..b609203 100644
--- a/java/src/com/google/idea/blaze/java/wizard2/BlazeNewProjectWizard.java
+++ b/java/src/com/google/idea/blaze/java/wizard2/BlazeNewProjectWizard.java
@@ -19,7 +19,7 @@
 import com.intellij.ide.util.newProjectWizard.AddModuleWizard;
 import com.intellij.projectImport.ProjectImportProvider;
 import java.awt.event.ActionListener;
-import org.jetbrains.annotations.Nullable;
+import javax.annotation.Nullable;
 
 final class BlazeNewProjectWizard extends AddModuleWizard {
   public BlazeNewProjectWizard(ProjectImportProvider provider) {
diff --git a/java/tests/integrationtests/com/google/idea/blaze/java/run/JUnitTestHeuristicTest.java b/java/tests/integrationtests/com/google/idea/blaze/java/run/JUnitTestHeuristicTest.java
new file mode 100644
index 0000000..5470d05
--- /dev/null
+++ b/java/tests/integrationtests/com/google/idea/blaze/java/run/JUnitTestHeuristicTest.java
@@ -0,0 +1,138 @@
+/*
+ * Copyright 2017 The Bazel Authors. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.idea.blaze.java.run;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.google.idea.blaze.base.BlazeIntegrationTestCase;
+import com.google.idea.blaze.base.ideinfo.TargetIdeInfo;
+import com.google.idea.blaze.base.model.primitives.WorkspacePath;
+import com.intellij.psi.PsiFile;
+import java.io.File;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** Integration tests for {@link JUnitTestHeuristic}. */
+@RunWith(JUnit4.class)
+public class JUnitTestHeuristicTest extends BlazeIntegrationTestCase {
+
+  @Before
+  public final void doSetup() {
+    // required for IntelliJ to recognize annotations, JUnit version, etc.
+    workspace.createPsiFile(
+        new WorkspacePath("org/junit/runner/RunWith.java"),
+        "package org.junit.runner;"
+            + "public @interface RunWith {"
+            + "    Class<? extends Runner> value();"
+            + "}");
+    workspace.createPsiFile(
+        new WorkspacePath("org/junit/Test"), "package org.junit;", "public @interface Test {}");
+    workspace.createPsiFile(
+        new WorkspacePath("org/junit/runners/JUnit4"),
+        "package org.junit.runners;",
+        "public class JUnit4 {}");
+  }
+
+  @Test
+  public void testMatchesJunit4Annotation() {
+    PsiFile psiFile =
+        workspace.createPsiFile(
+            new WorkspacePath("java/com/google/lib/JavaClass.java"),
+            "package com.google.lib;",
+            "import org.junit.Test;",
+            "import org.junit.runner.RunWith;",
+            "import org.junit.runners.JUnit4;",
+            "@RunWith(JUnit4.class)",
+            "public class JavaClass {",
+            "  @Test",
+            "  public void testMethod1() {}",
+            "  @Test",
+            "  public void testMethod2() {}",
+            "}");
+    File file = new File(psiFile.getVirtualFile().getPath());
+    TargetIdeInfo target =
+        TargetIdeInfo.builder().setLabel("//foo:AllJUnit4Tests").setKind("java_test").build();
+    assertThat(new JUnitTestHeuristic().matchesSource(getProject(), target, psiFile, file, null))
+        .isTrue();
+  }
+
+  @Test
+  public void testIgnoresJunit4AnnotationIfTargetNameDoesNotMatch() {
+    PsiFile psiFile =
+        workspace.createPsiFile(
+            new WorkspacePath("java/com/google/lib/JavaClass.java"),
+            "package com.google.lib;",
+            "import org.junit.Test;",
+            "import org.junit.runner.RunWith;",
+            "import org.junit.runners.JUnit4;",
+            "@RunWith(JUnit4.class)",
+            "public class JavaClass {",
+            "  @Test",
+            "  public void testMethod1() {}",
+            "  @Test",
+            "  public void testMethod2() {}",
+            "}");
+    File file = new File(psiFile.getVirtualFile().getPath());
+    TargetIdeInfo target =
+        TargetIdeInfo.builder().setLabel("//foo:unrelatedName").setKind("java_test").build();
+    assertThat(new JUnitTestHeuristic().matchesSource(getProject(), target, psiFile, file, null))
+        .isFalse();
+  }
+
+  @Test
+  public void testNonJavaFileDoesNotMatch() {
+    PsiFile psiFile = workspace.createPsiFile(new WorkspacePath("foo/script_test.py"));
+    File file = new File(psiFile.getVirtualFile().getPath());
+    TargetIdeInfo target =
+        TargetIdeInfo.builder().setLabel("//foo:unrelatedName").setKind("python_test").build();
+    assertThat(new JUnitTestHeuristic().matchesSource(getProject(), target, psiFile, file, null))
+        .isFalse();
+  }
+
+  @Test
+  public void testNullPsiFileDoesNotMatch() {
+    File file = new File("foo/script_test.py");
+    TargetIdeInfo target =
+        TargetIdeInfo.builder().setLabel("//foo:unrelatedName").setKind("python_test").build();
+    assertThat(new JUnitTestHeuristic().matchesSource(getProject(), target, null, file, null))
+        .isFalse();
+  }
+
+  @Test
+  public void testJunit4SourceDoesNotMatchJunit3TargetName() {
+    PsiFile psiFile =
+        workspace.createPsiFile(
+            new WorkspacePath("java/com/google/lib/JavaClass.java"),
+            "package com.google.lib;",
+            "import org.junit.Test;",
+            "import org.junit.runner.RunWith;",
+            "import org.junit.runners.JUnit4;",
+            "@RunWith(JUnit4.class)",
+            "public class JavaClass {",
+            "  @Test",
+            "  public void testMethod1() {}",
+            "  @Test",
+            "  public void testMethod2() {}",
+            "}");
+    File file = new File(psiFile.getVirtualFile().getPath());
+    TargetIdeInfo target =
+        TargetIdeInfo.builder().setLabel("//foo:AllJUnit3Tests").setKind("java_test").build();
+    assertThat(new JUnitTestHeuristic().matchesSource(getProject(), target, psiFile, file, null))
+        .isFalse();
+  }
+}
diff --git a/java/tests/integrationtests/com/google/idea/blaze/java/run/producers/BlazeJavaAbstractTestCaseConfigurationProducerTest.java b/java/tests/integrationtests/com/google/idea/blaze/java/run/producers/BlazeJavaAbstractTestCaseConfigurationProducerTest.java
new file mode 100644
index 0000000..b30fa3e
--- /dev/null
+++ b/java/tests/integrationtests/com/google/idea/blaze/java/run/producers/BlazeJavaAbstractTestCaseConfigurationProducerTest.java
@@ -0,0 +1,194 @@
+/*
+ * Copyright 2017 The Bazel Authors. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.idea.blaze.java.run.producers;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.google.idea.blaze.base.command.BlazeFlags;
+import com.google.idea.blaze.base.ideinfo.TargetIdeInfo;
+import com.google.idea.blaze.base.ideinfo.TargetMapBuilder;
+import com.google.idea.blaze.base.lang.buildfile.psi.util.PsiUtils;
+import com.google.idea.blaze.base.model.MockBlazeProjectDataBuilder;
+import com.google.idea.blaze.base.model.MockBlazeProjectDataManager;
+import com.google.idea.blaze.base.model.primitives.TargetExpression;
+import com.google.idea.blaze.base.model.primitives.WorkspacePath;
+import com.google.idea.blaze.base.run.BlazeCommandRunConfiguration;
+import com.google.idea.blaze.base.run.producer.BlazeRunConfigurationProducerTestCase;
+import com.google.idea.blaze.base.sync.data.BlazeProjectDataManager;
+import com.intellij.execution.actions.ConfigurationContext;
+import com.intellij.execution.actions.ConfigurationFromContext;
+import com.intellij.execution.configurations.RunConfiguration;
+import com.intellij.openapi.util.EmptyRunnable;
+import com.intellij.psi.PsiClass;
+import com.intellij.psi.PsiClassOwner;
+import com.intellij.psi.PsiFile;
+import com.intellij.psi.PsiMethod;
+import java.util.List;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** Integration tests for {@link BlazeJavaAbstractTestCaseConfigurationProducer}. */
+@RunWith(JUnit4.class)
+public class BlazeJavaAbstractTestCaseConfigurationProducerTest
+    extends BlazeRunConfigurationProducerTestCase {
+
+  @Test
+  public void testNonAbstractClassIgnored() {
+    PsiFile javaFile =
+        createAndIndexFile(
+            new WorkspacePath("java/com/google/test/TestClass.java"),
+            "package com.google.test;",
+            "@org.junit.runner.RunWith(org.junit.runners.JUnit4.class)",
+            "public class TestClass {",
+            "  @org.junit.Test",
+            "  public void testMethod1() {}",
+            "  @org.junit.Test",
+            "  public void testMethod2() {}",
+            "}");
+
+    PsiClass javaClass = ((PsiClassOwner) javaFile).getClasses()[0];
+    assertThat(javaClass).isNotNull();
+
+    ConfigurationContext context = createContextFromPsi(javaClass);
+    ConfigurationFromContext fromContext =
+        new BlazeJavaAbstractTestCaseConfigurationProducer()
+            .createConfigurationFromContext(context);
+    assertThat(fromContext).isNull();
+  }
+
+  @Test
+  public void testConfigurationCreatedFromAbstractClass() {
+    workspace.createPsiDirectory(new WorkspacePath("java/com/google/test"));
+    PsiFile abstractClassFile =
+        createAndIndexFile(
+            new WorkspacePath("java/com/google/test/AbstractTestCase.java"),
+            "package com.google.test;",
+            "public abstract class AbstractTestCase {}");
+
+    createAndIndexFile(
+        new WorkspacePath("java/com/google/test/TestClass.java"),
+        "package com.google.test;",
+        "import com.google.test.AbstractTestCase;",
+        "@org.junit.runner.RunWith(org.junit.runners.JUnit4.class)",
+        "public class TestClass extends AbstractTestCase {",
+        "  @org.junit.Test",
+        "  public void testMethod1() {}",
+        "  @org.junit.Test",
+        "  public void testMethod2() {}",
+        "}");
+
+    PsiClass javaClass = ((PsiClassOwner) abstractClassFile).getClasses()[0];
+    assertThat(javaClass).isNotNull();
+
+    ConfigurationContext context = createContextFromPsi(abstractClassFile);
+    List<ConfigurationFromContext> configurations = context.getConfigurationsFromContext();
+    assertThat(configurations).hasSize(1);
+
+    ConfigurationFromContext fromContext = configurations.get(0);
+    assertThat(fromContext.isProducedBy(BlazeJavaAbstractTestCaseConfigurationProducer.class))
+        .isTrue();
+    assertThat(fromContext.getSourceElement()).isEqualTo(javaClass);
+
+    RunConfiguration config = fromContext.getConfiguration();
+    assertThat(config).isInstanceOf(BlazeCommandRunConfiguration.class);
+    BlazeCommandRunConfiguration blazeConfig = (BlazeCommandRunConfiguration) config;
+    assertThat(blazeConfig.getTarget()).isNull();
+    assertThat(blazeConfig.getName()).isEqualTo("AbstractTestCase");
+
+    MockBlazeProjectDataBuilder builder = MockBlazeProjectDataBuilder.builder(workspaceRoot);
+    builder.setTargetMap(
+        TargetMapBuilder.builder()
+            .addTarget(
+                TargetIdeInfo.builder()
+                    .setKind("java_test")
+                    .setLabel("//java/com/google/test:TestClass")
+                    .addSource(sourceRoot("java/com/google/test/TestClass.java"))
+                    .build())
+            .build());
+    registerProjectService(
+        BlazeProjectDataManager.class, new MockBlazeProjectDataManager(builder.build()));
+
+    BlazeJavaAbstractTestCaseConfigurationProducer.chooseSubclass(
+        fromContext, context, EmptyRunnable.INSTANCE);
+
+    assertThat(blazeConfig.getTarget())
+        .isEqualTo(TargetExpression.fromString("//java/com/google/test:TestClass"));
+    assertThat(getTestFilterContents(blazeConfig))
+        .isEqualTo(BlazeFlags.TEST_FILTER + "=com.google.test.TestClass#");
+  }
+
+  @Test
+  public void testConfigurationCreatedFromMethodInAbstractClass() {
+    PsiFile abstractClassFile =
+        createAndIndexFile(
+            new WorkspacePath("java/com/google/test/AbstractTestCase.java"),
+            "package com.google.test;",
+            "public abstract class AbstractTestCase {",
+            "  @org.junit.Test",
+            "  public void testMethod() {}",
+            "}");
+
+    createAndIndexFile(
+        new WorkspacePath("java/com/google/test/TestClass.java"),
+        "package com.google.test;",
+        "import com.google.test.AbstractTestCase;",
+        "import org.junit.runner.RunWith;",
+        "import org.junit.runners.JUnit4;",
+        "@org.junit.runner.RunWith(org.junit.runners.JUnit4.class)",
+        "public class TestClass extends AbstractTestCase {}");
+
+    PsiClass javaClass = ((PsiClassOwner) abstractClassFile).getClasses()[0];
+    PsiMethod method = PsiUtils.findFirstChildOfClassRecursive(javaClass, PsiMethod.class);
+    assertThat(method).isNotNull();
+
+    ConfigurationContext context = createContextFromPsi(method);
+    List<ConfigurationFromContext> configurations = context.getConfigurationsFromContext();
+    assertThat(configurations).hasSize(1);
+
+    ConfigurationFromContext fromContext = configurations.get(0);
+    assertThat(fromContext.isProducedBy(BlazeJavaAbstractTestCaseConfigurationProducer.class))
+        .isTrue();
+    assertThat(fromContext.getSourceElement()).isEqualTo(method);
+
+    RunConfiguration config = fromContext.getConfiguration();
+    assertThat(config).isInstanceOf(BlazeCommandRunConfiguration.class);
+    BlazeCommandRunConfiguration blazeConfig = (BlazeCommandRunConfiguration) config;
+    assertThat(blazeConfig.getTarget()).isNull();
+    assertThat(blazeConfig.getName()).isEqualTo("AbstractTestCase.testMethod");
+
+    MockBlazeProjectDataBuilder builder = MockBlazeProjectDataBuilder.builder(workspaceRoot);
+    builder.setTargetMap(
+        TargetMapBuilder.builder()
+            .addTarget(
+                TargetIdeInfo.builder()
+                    .setKind("java_test")
+                    .setLabel("//java/com/google/test:TestClass")
+                    .addSource(sourceRoot("java/com/google/test/TestClass.java"))
+                    .build())
+            .build());
+    registerProjectService(
+        BlazeProjectDataManager.class, new MockBlazeProjectDataManager(builder.build()));
+
+    BlazeJavaAbstractTestCaseConfigurationProducer.chooseSubclass(
+        fromContext, context, EmptyRunnable.INSTANCE);
+
+    assertThat(blazeConfig.getTarget())
+        .isEqualTo(TargetExpression.fromString("//java/com/google/test:TestClass"));
+    assertThat(getTestFilterContents(blazeConfig))
+        .isEqualTo(BlazeFlags.TEST_FILTER + "=com.google.test.TestClass#testMethod$");
+  }
+}
diff --git a/java/tests/integrationtests/com/google/idea/blaze/java/run/producers/BlazeJavaMainClassConfigurationProducerTest.java b/java/tests/integrationtests/com/google/idea/blaze/java/run/producers/BlazeJavaMainClassConfigurationProducerTest.java
index 6135b2f..87c8f28 100644
--- a/java/tests/integrationtests/com/google/idea/blaze/java/run/producers/BlazeJavaMainClassConfigurationProducerTest.java
+++ b/java/tests/integrationtests/com/google/idea/blaze/java/run/producers/BlazeJavaMainClassConfigurationProducerTest.java
@@ -17,64 +17,31 @@
 
 import static com.google.common.truth.Truth.assertThat;
 
-import com.google.common.collect.ImmutableList;
-import com.google.idea.blaze.base.BlazeIntegrationTestCase;
-import com.google.idea.blaze.base.EditorTestHelper;
-import com.google.idea.blaze.base.ideinfo.ArtifactLocation;
 import com.google.idea.blaze.base.ideinfo.JavaIdeInfo;
 import com.google.idea.blaze.base.ideinfo.TargetIdeInfo;
-import com.google.idea.blaze.base.ideinfo.TargetMap;
 import com.google.idea.blaze.base.ideinfo.TargetMapBuilder;
 import com.google.idea.blaze.base.model.MockBlazeProjectDataBuilder;
 import com.google.idea.blaze.base.model.MockBlazeProjectDataManager;
-import com.google.idea.blaze.base.model.primitives.ExecutionRootPath;
 import com.google.idea.blaze.base.model.primitives.TargetExpression;
 import com.google.idea.blaze.base.model.primitives.WorkspacePath;
 import com.google.idea.blaze.base.run.BlazeRunConfiguration;
-import com.google.idea.blaze.base.sync.SyncCache;
+import com.google.idea.blaze.base.run.producer.BlazeRunConfigurationProducerTestCase;
 import com.google.idea.blaze.base.sync.data.BlazeProjectDataManager;
-import com.google.idea.blaze.base.sync.workspace.BlazeRoots;
-import com.intellij.execution.Location;
-import com.intellij.execution.PsiLocation;
-import com.intellij.execution.RunnerAndConfigurationSettings;
-import com.intellij.execution.actions.ConfigurationContext;
 import com.intellij.execution.configurations.RunConfiguration;
-import com.intellij.openapi.actionSystem.CommonDataKeys;
-import com.intellij.openapi.actionSystem.LangDataKeys;
-import com.intellij.openapi.module.ModuleUtil;
-import com.intellij.openapi.vfs.VirtualFile;
 import com.intellij.psi.PsiFile;
-import com.intellij.testFramework.MapDataContext;
-import java.io.File;
-import org.jetbrains.annotations.Nullable;
-import org.junit.After;
-import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.junit.runners.JUnit4;
 
-/** Integration tests for {@link BlazeJavaMainClassConfigurationProducer}. */
+/** Integration tests for {@link BlazeJavaMainClassRunConfigurationProducer}. */
 @RunWith(JUnit4.class)
-public class BlazeJavaMainClassConfigurationProducerTest extends BlazeIntegrationTestCase {
-
-  private EditorTestHelper editorTest;
-
-  @Before
-  public final void doSetup() {
-    BlazeProjectDataManager mockProjectDataManager =
-        new MockBlazeProjectDataManager(getMockBlazeProjectDataBuilder().build());
-    registerProjectService(BlazeProjectDataManager.class, mockProjectDataManager);
-    editorTest = new EditorTestHelper(getProject(), testFixture);
-  }
-
-  @After
-  public final void doTearDown() {
-    SyncCache.getInstance(getProject()).clear();
-  }
+public class BlazeJavaMainClassConfigurationProducerTest
+    extends BlazeRunConfigurationProducerTestCase {
 
   @Test
   public void testUniqueJavaBinaryChosen() {
-    setTargets(
+    MockBlazeProjectDataBuilder builder = MockBlazeProjectDataBuilder.builder(workspaceRoot);
+    builder.setTargetMap(
         TargetMapBuilder.builder()
             .addTarget(
                 TargetIdeInfo.builder()
@@ -83,14 +50,16 @@
                     .addSource(sourceRoot("com/google/binary/MainClass.java"))
                     .build())
             .build());
+    registerProjectService(
+        BlazeProjectDataManager.class, new MockBlazeProjectDataManager(builder.build()));
 
     PsiFile javaClass =
-        workspace.createPsiFile(
+        createAndIndexFile(
             WorkspacePath.createIfValid("com/google/binary/MainClass.java"),
             "package com.google.binary;",
             "import java.lang.String;",
             "public class MainClass {",
-            "  public static void main(String[] args) {}",
+            "  public static void main(java.lang.String[] args) {}",
             "}");
 
     RunConfiguration config = createConfigurationFromLocation(javaClass);
@@ -103,7 +72,8 @@
 
   @Test
   public void testNoJavaBinaryChosenIfNotInRDeps() {
-    setTargets(
+    MockBlazeProjectDataBuilder builder = MockBlazeProjectDataBuilder.builder(workspaceRoot);
+    builder.setTargetMap(
         TargetMapBuilder.builder()
             .addTarget(
                 TargetIdeInfo.builder()
@@ -112,14 +82,16 @@
                     .addSource(sourceRoot("com/google/binary/OtherClass.java"))
                     .build())
             .build());
+    registerProjectService(
+        BlazeProjectDataManager.class, new MockBlazeProjectDataManager(builder.build()));
 
     PsiFile javaClass =
-        workspace.createPsiFile(
+        createAndIndexFile(
             WorkspacePath.createIfValid("com/google/binary/MainClass.java"),
             "package com.google.binary;",
             "import java.lang.String;",
             "public class MainClass {",
-            "  public static void main(String[] args) {}",
+            "  public static void main(java.lang.String[] args) {}",
             "}");
 
     assertThat(createConfigurationFromLocation(javaClass))
@@ -128,7 +100,8 @@
 
   @Test
   public void testNoResultForClassWithoutMainMethod() {
-    setTargets(
+    MockBlazeProjectDataBuilder builder = MockBlazeProjectDataBuilder.builder(workspaceRoot);
+    builder.setTargetMap(
         TargetMapBuilder.builder()
             .addTarget(
                 TargetIdeInfo.builder()
@@ -138,9 +111,11 @@
                     .setJavaInfo(JavaIdeInfo.builder().setMainClass("com.google.binary.MainClass"))
                     .build())
             .build());
+    registerProjectService(
+        BlazeProjectDataManager.class, new MockBlazeProjectDataManager(builder.build()));
 
     PsiFile javaClass =
-        workspace.createPsiFile(
+        createAndIndexFile(
             WorkspacePath.createIfValid("com/google/binary/MainClass.java"),
             "package com.google.binary;",
             "public class MainClass {}");
@@ -150,7 +125,8 @@
 
   @Test
   public void testJavaBinaryWithMatchingNameChosen() {
-    setTargets(
+    MockBlazeProjectDataBuilder builder = MockBlazeProjectDataBuilder.builder(workspaceRoot);
+    builder.setTargetMap(
         TargetMapBuilder.builder()
             .addTarget(
                 TargetIdeInfo.builder()
@@ -165,14 +141,16 @@
                     .addSource(sourceRoot("com/google/binary/MainClass.java"))
                     .build())
             .build());
+    registerProjectService(
+        BlazeProjectDataManager.class, new MockBlazeProjectDataManager(builder.build()));
 
     PsiFile javaClass =
-        workspace.createPsiFile(
+        createAndIndexFile(
             WorkspacePath.createIfValid("com/google/binary/MainClass.java"),
             "package com.google.binary;",
             "import java.lang.String;",
             "public class MainClass {",
-            "  public static void main(String[] args) {}",
+            "  public static void main(java.lang.String[] args) {}",
             "}");
 
     RunConfiguration config = createConfigurationFromLocation(javaClass);
@@ -184,7 +162,8 @@
 
   @Test
   public void testJavaBinaryWithMatchingMainClassChosen() {
-    setTargets(
+    MockBlazeProjectDataBuilder builder = MockBlazeProjectDataBuilder.builder(workspaceRoot);
+    builder.setTargetMap(
         TargetMapBuilder.builder()
             .addTarget(
                 TargetIdeInfo.builder()
@@ -200,14 +179,16 @@
                     .addSource(sourceRoot("com/google/binary/MainClass.java"))
                     .build())
             .build());
+    registerProjectService(
+        BlazeProjectDataManager.class, new MockBlazeProjectDataManager(builder.build()));
 
     PsiFile javaClass =
-        workspace.createPsiFile(
+        createAndIndexFile(
             WorkspacePath.createIfValid("com/google/binary/MainClass.java"),
             "package com.google.binary;",
             "import java.lang.String;",
             "public class MainClass {",
-            "  public static void main(String[] args) {}",
+            "  public static void main(java.lang.String[] args) {}",
             "}");
 
     RunConfiguration config = createConfigurationFromLocation(javaClass);
@@ -217,48 +198,4 @@
     assertThat(blazeConfig.getTarget())
         .isEqualTo(TargetExpression.fromString("//com/google/binary:OtherName"));
   }
-
-  @Nullable
-  private RunConfiguration createConfigurationFromLocation(PsiFile psiFile) {
-    // a nauseating hack to force IntelliJ to recognize 'main' methods...
-    workspace.createPsiFile(
-        WorkspacePath.createIfValid("java/lang/String.java"),
-        "package java.lang;",
-        "public class String {}");
-    editorTest.openFileInEditor(psiFile);
-
-    final MapDataContext dataContext = new MapDataContext();
-
-    dataContext.put(CommonDataKeys.PROJECT, getProject());
-    dataContext.put(LangDataKeys.MODULE, ModuleUtil.findModuleForPsiElement(psiFile));
-    dataContext.put(Location.DATA_KEY, PsiLocation.fromPsiElement(psiFile));
-    RunnerAndConfigurationSettings settings =
-        ConfigurationContext.getFromContext(dataContext).getConfiguration();
-    return settings != null ? settings.getConfiguration() : null;
-  }
-
-  private MockBlazeProjectDataBuilder getMockBlazeProjectDataBuilder() {
-    String executionRootPath = "usr/local/_blaze_";
-    VirtualFile vf = fileSystem.createDirectory(executionRootPath);
-    BlazeRoots fakeRoots =
-        new BlazeRoots(
-            new File(vf.getPath()),
-            ImmutableList.of(workspaceRoot.directory()),
-            new ExecutionRootPath("out/crosstool/bin"),
-            new ExecutionRootPath("out/crosstool/gen"),
-            null);
-    return MockBlazeProjectDataBuilder.builder(workspaceRoot).setBlazeRoots(fakeRoots);
-  }
-
-  private void setTargets(TargetMap targets) {
-    BlazeProjectDataManager mockProjectDataManager =
-        new MockBlazeProjectDataManager(
-            getMockBlazeProjectDataBuilder().setTargetMap(targets).build());
-    registerProjectService(BlazeProjectDataManager.class, mockProjectDataManager);
-    SyncCache.getInstance(getProject()).clear();
-  }
-
-  private static ArtifactLocation sourceRoot(String relativePath) {
-    return ArtifactLocation.builder().setRelativePath(relativePath).setIsSource(true).build();
-  }
 }
diff --git a/java/tests/integrationtests/com/google/idea/blaze/java/run/producers/BlazeJavaTestClassConfigurationProducerTest.java b/java/tests/integrationtests/com/google/idea/blaze/java/run/producers/BlazeJavaTestClassConfigurationProducerTest.java
new file mode 100644
index 0000000..6c48f2f
--- /dev/null
+++ b/java/tests/integrationtests/com/google/idea/blaze/java/run/producers/BlazeJavaTestClassConfigurationProducerTest.java
@@ -0,0 +1,188 @@
+/*
+ * Copyright 2017 The Bazel Authors. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.idea.blaze.java.run.producers;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.google.idea.blaze.base.command.BlazeCommandName;
+import com.google.idea.blaze.base.ideinfo.TargetIdeInfo;
+import com.google.idea.blaze.base.ideinfo.TargetMapBuilder;
+import com.google.idea.blaze.base.model.MockBlazeProjectDataBuilder;
+import com.google.idea.blaze.base.model.MockBlazeProjectDataManager;
+import com.google.idea.blaze.base.model.primitives.TargetExpression;
+import com.google.idea.blaze.base.model.primitives.WorkspacePath;
+import com.google.idea.blaze.base.run.BlazeCommandRunConfiguration;
+import com.google.idea.blaze.base.run.producer.BlazeRunConfigurationProducerTestCase;
+import com.google.idea.blaze.base.sync.data.BlazeProjectDataManager;
+import com.intellij.execution.actions.ConfigurationContext;
+import com.intellij.execution.actions.ConfigurationFromContext;
+import com.intellij.psi.PsiClass;
+import com.intellij.psi.PsiClassOwner;
+import com.intellij.psi.PsiFile;
+import java.util.List;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** Integration tests for {@link BlazeJavaTestClassConfigurationProducer}. */
+@RunWith(JUnit4.class)
+public class BlazeJavaTestClassConfigurationProducerTest
+    extends BlazeRunConfigurationProducerTestCase {
+
+  @Test
+  public void testProducedFromPsiFile() {
+    PsiFile javaFile =
+        createAndIndexFile(
+            new WorkspacePath("java/com/google/test/TestClass.java"),
+            "package com.google.test;",
+            "@org.junit.runner.RunWith(org.junit.runners.JUnit4.class)",
+            "public class TestClass {",
+            "  @org.junit.Test",
+            "  public void testMethod1() {}",
+            "  @org.junit.Test",
+            "  public void testMethod2() {}",
+            "}");
+
+    MockBlazeProjectDataBuilder builder = MockBlazeProjectDataBuilder.builder(workspaceRoot);
+    builder.setTargetMap(
+        TargetMapBuilder.builder()
+            .addTarget(
+                TargetIdeInfo.builder()
+                    .setKind("java_test")
+                    .setLabel("//java/com/google/test:TestClass")
+                    .addSource(sourceRoot("java/com/google/test/TestClass.java"))
+                    .build())
+            .build());
+    registerProjectService(
+        BlazeProjectDataManager.class, new MockBlazeProjectDataManager(builder.build()));
+
+    ConfigurationContext context = createContextFromPsi(javaFile);
+    List<ConfigurationFromContext> configurations = context.getConfigurationsFromContext();
+    assertThat(configurations).hasSize(1);
+
+    ConfigurationFromContext fromContext = configurations.get(0);
+    assertThat(fromContext.isProducedBy(BlazeJavaTestClassConfigurationProducer.class)).isTrue();
+    assertThat(fromContext.getConfiguration()).isInstanceOf(BlazeCommandRunConfiguration.class);
+
+    BlazeCommandRunConfiguration config =
+        (BlazeCommandRunConfiguration) fromContext.getConfiguration();
+    assertThat(config.getTarget())
+        .isEqualTo(TargetExpression.fromString("//java/com/google/test:TestClass"));
+    assertThat(getTestFilterContents(config)).isEqualTo("--test_filter=com.google.test.TestClass#");
+    assertThat(config.getName()).isEqualTo("Blaze test TestClass");
+    assertThat(getCommandType(config)).isEqualTo(BlazeCommandName.TEST);
+  }
+
+  @Test
+  public void testProducedFromPsiClass() {
+    PsiFile javaFile =
+        createAndIndexFile(
+            new WorkspacePath("java/com/google/test/TestClass.java"),
+            "package com.google.test;",
+            "@org.junit.runner.RunWith(org.junit.runners.JUnit4.class)",
+            "public class TestClass {",
+            "  @org.junit.Test",
+            "  public void testMethod1() {}",
+            "  @org.junit.Test",
+            "  public void testMethod2() {}",
+            "}");
+
+    MockBlazeProjectDataBuilder builder = MockBlazeProjectDataBuilder.builder(workspaceRoot);
+    builder.setTargetMap(
+        TargetMapBuilder.builder()
+            .addTarget(
+                TargetIdeInfo.builder()
+                    .setKind("java_test")
+                    .setLabel("//java/com/google/test:TestClass")
+                    .addSource(sourceRoot("java/com/google/test/TestClass.java"))
+                    .build())
+            .build());
+    registerProjectService(
+        BlazeProjectDataManager.class, new MockBlazeProjectDataManager(builder.build()));
+
+    PsiClass javaClass = ((PsiClassOwner) javaFile).getClasses()[0];
+    assertThat(javaClass).isNotNull();
+
+    ConfigurationContext context = createContextFromPsi(javaClass);
+    List<ConfigurationFromContext> configurations = context.getConfigurationsFromContext();
+    assertThat(configurations).hasSize(1);
+
+    ConfigurationFromContext fromContext = configurations.get(0);
+    assertThat(fromContext.isProducedBy(BlazeJavaTestClassConfigurationProducer.class)).isTrue();
+    assertThat(fromContext.getConfiguration()).isInstanceOf(BlazeCommandRunConfiguration.class);
+
+    BlazeCommandRunConfiguration config =
+        (BlazeCommandRunConfiguration) fromContext.getConfiguration();
+    assertThat(config.getTarget())
+        .isEqualTo(TargetExpression.fromString("//java/com/google/test:TestClass"));
+    assertThat(getTestFilterContents(config)).isEqualTo("--test_filter=com.google.test.TestClass#");
+    assertThat(config.getName()).isEqualTo("Blaze test TestClass");
+    assertThat(getCommandType(config)).isEqualTo(BlazeCommandName.TEST);
+  }
+
+  @Test
+  public void testProducedFromPsiClassWithInnerTestClass() {
+    PsiFile javaFile =
+        createAndIndexFile(
+            new WorkspacePath("java/com/google/test/OuterClass.java"),
+            "package com.google.test;",
+            "@org.junit.runner.RunWith(org.junit.runners.Suite.class)",
+            "@org.junit.runners.Suite.SuiteClasses({OuterClass.InnerClass.class})",
+            "public class OuterClass {",
+            "  @org.junit.runner.RunWith(org.junit.runners.JUnit4.class)",
+            "  public static class InnerClass {",
+            "    @org.junit.Test",
+            "    public void testMethod1() {}",
+            "    @org.junit.Test",
+            "    public void testMethod2() {}",
+            "  }",
+            "}");
+
+    MockBlazeProjectDataBuilder builder = MockBlazeProjectDataBuilder.builder(workspaceRoot);
+    builder.setTargetMap(
+        TargetMapBuilder.builder()
+            .addTarget(
+                TargetIdeInfo.builder()
+                    .setKind("java_test")
+                    .setLabel("//java/com/google/test:OuterClass")
+                    .addSource(sourceRoot("java/com/google/test/OuterClass.java"))
+                    .build())
+            .build());
+    registerProjectService(
+        BlazeProjectDataManager.class, new MockBlazeProjectDataManager(builder.build()));
+
+    PsiClass javaClass = ((PsiClassOwner) javaFile).getClasses()[0];
+    assertThat(javaClass).isNotNull();
+
+    ConfigurationContext context = createContextFromPsi(javaClass);
+    List<ConfigurationFromContext> configurations = context.getConfigurationsFromContext();
+    assertThat(configurations).hasSize(1);
+
+    ConfigurationFromContext fromContext = configurations.get(0);
+    assertThat(fromContext.isProducedBy(BlazeJavaTestClassConfigurationProducer.class)).isTrue();
+    assertThat(fromContext.getConfiguration()).isInstanceOf(BlazeCommandRunConfiguration.class);
+
+    BlazeCommandRunConfiguration config =
+        (BlazeCommandRunConfiguration) fromContext.getConfiguration();
+    assertThat(config.getTarget())
+        .isEqualTo(TargetExpression.fromString("//java/com/google/test:OuterClass"));
+    assertThat(getTestFilterContents(config))
+        .isEqualTo(
+            "--test_filter=com.google.test.OuterClass#|com.google.test.OuterClass.InnerClass#");
+    assertThat(config.getName()).isEqualTo("Blaze test OuterClass");
+    assertThat(getCommandType(config)).isEqualTo(BlazeCommandName.TEST);
+  }
+}
diff --git a/java/tests/integrationtests/com/google/idea/blaze/java/run/producers/BlazeJavaTestMethodConfigurationProducerTest.java b/java/tests/integrationtests/com/google/idea/blaze/java/run/producers/BlazeJavaTestMethodConfigurationProducerTest.java
new file mode 100644
index 0000000..aa7d2a2
--- /dev/null
+++ b/java/tests/integrationtests/com/google/idea/blaze/java/run/producers/BlazeJavaTestMethodConfigurationProducerTest.java
@@ -0,0 +1,96 @@
+/*
+ * Copyright 2017 The Bazel Authors. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.idea.blaze.java.run.producers;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.google.idea.blaze.base.command.BlazeCommandName;
+import com.google.idea.blaze.base.command.BlazeFlags;
+import com.google.idea.blaze.base.ideinfo.TargetIdeInfo;
+import com.google.idea.blaze.base.ideinfo.TargetMapBuilder;
+import com.google.idea.blaze.base.lang.buildfile.psi.util.PsiUtils;
+import com.google.idea.blaze.base.model.MockBlazeProjectDataBuilder;
+import com.google.idea.blaze.base.model.MockBlazeProjectDataManager;
+import com.google.idea.blaze.base.model.primitives.TargetExpression;
+import com.google.idea.blaze.base.model.primitives.WorkspacePath;
+import com.google.idea.blaze.base.run.BlazeCommandRunConfiguration;
+import com.google.idea.blaze.base.run.producer.BlazeRunConfigurationProducerTestCase;
+import com.google.idea.blaze.base.run.state.BlazeCommandRunConfigurationCommonState;
+import com.google.idea.blaze.base.sync.data.BlazeProjectDataManager;
+import com.intellij.execution.actions.ConfigurationContext;
+import com.intellij.execution.actions.ConfigurationFromContext;
+import com.intellij.psi.PsiFile;
+import com.intellij.psi.PsiMethod;
+import java.util.List;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** Integration tests for {@link BlazeJavaTestMethodConfigurationProducer}. */
+@RunWith(JUnit4.class)
+public class BlazeJavaTestMethodConfigurationProducerTest
+    extends BlazeRunConfigurationProducerTestCase {
+
+  @Test
+  public void testProducedFromPsiMethod() {
+    PsiFile javaFile =
+        createAndIndexFile(
+            new WorkspacePath("java/com/google/test/TestClass.java"),
+            "package com.google.test;",
+            "@org.junit.runner.RunWith(org.junit.runners.JUnit4.class)",
+            "public class TestClass {",
+            "  @org.junit.Test",
+            "  public void testMethod1() {}",
+            "}");
+
+    MockBlazeProjectDataBuilder builder = MockBlazeProjectDataBuilder.builder(workspaceRoot);
+    builder.setTargetMap(
+        TargetMapBuilder.builder()
+            .addTarget(
+                TargetIdeInfo.builder()
+                    .setKind("java_test")
+                    .setLabel("//java/com/google/test:TestClass")
+                    .addSource(sourceRoot("java/com/google/test/TestClass.java"))
+                    .build())
+            .build());
+    registerProjectService(
+        BlazeProjectDataManager.class, new MockBlazeProjectDataManager(builder.build()));
+
+    PsiMethod method = PsiUtils.findFirstChildOfClassRecursive(javaFile, PsiMethod.class);
+    assertThat(method).isNotNull();
+
+    ConfigurationContext context = createContextFromPsi(method);
+    List<ConfigurationFromContext> configurations = context.getConfigurationsFromContext();
+    assertThat(configurations).hasSize(1);
+
+    ConfigurationFromContext fromContext = configurations.get(0);
+    assertThat(fromContext.isProducedBy(BlazeJavaTestMethodConfigurationProducer.class)).isTrue();
+    assertThat(fromContext.getConfiguration()).isInstanceOf(BlazeCommandRunConfiguration.class);
+
+    BlazeCommandRunConfiguration config =
+        (BlazeCommandRunConfiguration) fromContext.getConfiguration();
+    assertThat(config.getTarget())
+        .isEqualTo(TargetExpression.fromString("//java/com/google/test:TestClass"));
+    assertThat(getTestFilterContents(config))
+        .isEqualTo("--test_filter=com.google.test.TestClass#testMethod1$");
+    assertThat(config.getName()).isEqualTo("Blaze test TestClass.testMethod1");
+    assertThat(getCommandType(config)).isEqualTo(BlazeCommandName.TEST);
+
+    BlazeCommandRunConfigurationCommonState state =
+        config.getHandlerStateIfType(BlazeCommandRunConfigurationCommonState.class);
+    assertThat(state.getBlazeFlagsState().getRawFlags()).contains(BlazeFlags.DISABLE_TEST_SHARDING);
+  }
+}
diff --git a/java/tests/integrationtests/com/google/idea/blaze/java/run/producers/MultipleJavaClassesTestConfigurationProducerTest.java b/java/tests/integrationtests/com/google/idea/blaze/java/run/producers/MultipleJavaClassesTestConfigurationProducerTest.java
new file mode 100644
index 0000000..731743a
--- /dev/null
+++ b/java/tests/integrationtests/com/google/idea/blaze/java/run/producers/MultipleJavaClassesTestConfigurationProducerTest.java
@@ -0,0 +1,409 @@
+/*
+ * Copyright 2017 The Bazel Authors. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.idea.blaze.java.run.producers;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.google.idea.blaze.base.command.BlazeCommandName;
+import com.google.idea.blaze.base.ideinfo.TargetIdeInfo;
+import com.google.idea.blaze.base.ideinfo.TargetMapBuilder;
+import com.google.idea.blaze.base.model.MockBlazeProjectDataBuilder;
+import com.google.idea.blaze.base.model.MockBlazeProjectDataManager;
+import com.google.idea.blaze.base.model.primitives.TargetExpression;
+import com.google.idea.blaze.base.model.primitives.WorkspacePath;
+import com.google.idea.blaze.base.run.BlazeCommandRunConfiguration;
+import com.google.idea.blaze.base.run.producer.BlazeRunConfigurationProducerTestCase;
+import com.google.idea.blaze.base.sync.data.BlazeProjectDataManager;
+import com.intellij.execution.actions.ConfigurationContext;
+import com.intellij.execution.actions.ConfigurationFromContext;
+import com.intellij.openapi.application.ApplicationManager;
+import com.intellij.openapi.roots.ContentEntry;
+import com.intellij.openapi.roots.ModifiableRootModel;
+import com.intellij.openapi.roots.ModuleRootManager;
+import com.intellij.openapi.roots.SourceFolder;
+import com.intellij.openapi.vfs.VirtualFile;
+import com.intellij.psi.PsiDirectory;
+import com.intellij.psi.PsiElement;
+import com.intellij.psi.PsiFile;
+import java.util.List;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** Integration tests for {@link MultipleJavaClassesTestConfigurationProducer}. */
+@RunWith(JUnit4.class)
+public class MultipleJavaClassesTestConfigurationProducerTest
+    extends BlazeRunConfigurationProducerTestCase {
+
+  private SourceFolder javaSourceRoot;
+
+  @Before
+  public final void addSourceFolder() {
+    // create a source root so that the package prefixes are correct.
+    VirtualFile pkgRoot = workspace.createDirectory(new WorkspacePath("java"));
+    ApplicationManager.getApplication()
+        .runWriteAction(
+            () -> {
+              final ModifiableRootModel model =
+                  ModuleRootManager.getInstance(testFixture.getModule()).getModifiableModel();
+              ContentEntry contentEntry = model.getContentEntries()[0];
+              javaSourceRoot = contentEntry.addSourceFolder(pkgRoot, false, "");
+              model.commit();
+            });
+
+    BlazeProjectDataManager mockProjectDataManager =
+        new MockBlazeProjectDataManager(MockBlazeProjectDataBuilder.builder(workspaceRoot).build());
+    registerProjectService(BlazeProjectDataManager.class, mockProjectDataManager);
+  }
+
+  @After
+  public final void removeSourceFolder() {
+    ApplicationManager.getApplication()
+        .runWriteAction(
+            () -> {
+              final ModifiableRootModel model =
+                  ModuleRootManager.getInstance(testFixture.getModule()).getModifiableModel();
+              ContentEntry contentEntry = model.getContentEntries()[0];
+              contentEntry.removeSourceFolder(javaSourceRoot);
+              model.commit();
+            });
+  }
+
+  @Test
+  public void testProducedFromDirectory() {
+    MockBlazeProjectDataBuilder builder = MockBlazeProjectDataBuilder.builder(workspaceRoot);
+    builder.setTargetMap(
+        TargetMapBuilder.builder()
+            .addTarget(
+                TargetIdeInfo.builder()
+                    .setKind("java_test")
+                    .setLabel("//java/com/google/test:TestClass")
+                    .addSource(sourceRoot("java/com/google/test/TestClass.java"))
+                    .build())
+            .build());
+    registerProjectService(
+        BlazeProjectDataManager.class, new MockBlazeProjectDataManager(builder.build()));
+
+    PsiDirectory directory =
+        workspace.createPsiDirectory(new WorkspacePath("java/com/google/test"));
+    createAndIndexFile(
+        new WorkspacePath("java/com/google/test/TestClass.java"),
+        "package com.google.test;",
+        "@org.junit.runner.RunWith(org.junit.runners.JUnit4.class)",
+        "public class TestClass {",
+        "  @org.junit.Test",
+        "  public void testMethod() {}",
+        "}");
+
+    ConfigurationContext context = createContextFromPsi(directory);
+    List<ConfigurationFromContext> configurations = context.getConfigurationsFromContext();
+    assertThat(configurations).hasSize(1);
+
+    ConfigurationFromContext fromContext = configurations.get(0);
+    assertThat(fromContext.isProducedBy(MultipleJavaClassesTestConfigurationProducer.class))
+        .isTrue();
+    assertThat(fromContext.getConfiguration()).isInstanceOf(BlazeCommandRunConfiguration.class);
+
+    BlazeCommandRunConfiguration config =
+        (BlazeCommandRunConfiguration) fromContext.getConfiguration();
+    assertThat(config.getTarget())
+        .isEqualTo(TargetExpression.fromString("//java/com/google/test:TestClass"));
+    assertThat(getTestFilterContents(config)).isEqualTo("--test_filter=com.google.test");
+    assertThat(config.getName()).isEqualTo("Blaze test all in directory 'test'");
+    assertThat(getCommandType(config)).isEqualTo(BlazeCommandName.TEST);
+  }
+
+  @Test
+  public void testProducedFromDirectoryWithNestedTests() {
+    MockBlazeProjectDataBuilder builder = MockBlazeProjectDataBuilder.builder(workspaceRoot);
+    builder.setTargetMap(
+        TargetMapBuilder.builder()
+            .addTarget(
+                TargetIdeInfo.builder()
+                    .setKind("java_test")
+                    .setLabel("//java/com/google/test:TestClass")
+                    .addSource(sourceRoot("java/com/google/test/TestClass.java"))
+                    .build())
+            .build());
+    registerProjectService(
+        BlazeProjectDataManager.class, new MockBlazeProjectDataManager(builder.build()));
+
+    PsiDirectory directory = workspace.createPsiDirectory(new WorkspacePath("java/com/google"));
+    createAndIndexFile(
+        new WorkspacePath("java/com/google/test/TestClass.java"),
+        "package com.google.test;",
+        "@org.junit.runner.RunWith(org.junit.runners.JUnit4.class)",
+        "public class TestClass {",
+        "  @org.junit.Test",
+        "  public void testMethod() {}",
+        "}");
+
+    ConfigurationContext context = createContextFromPsi(directory);
+    List<ConfigurationFromContext> configurations = context.getConfigurationsFromContext();
+    assertThat(configurations).hasSize(1);
+
+    ConfigurationFromContext fromContext = configurations.get(0);
+    assertThat(fromContext.isProducedBy(MultipleJavaClassesTestConfigurationProducer.class))
+        .isTrue();
+    assertThat(fromContext.getConfiguration()).isInstanceOf(BlazeCommandRunConfiguration.class);
+
+    BlazeCommandRunConfiguration config =
+        (BlazeCommandRunConfiguration) fromContext.getConfiguration();
+    assertThat(config.getTarget())
+        .isEqualTo(TargetExpression.fromString("//java/com/google/test:TestClass"));
+    assertThat(getTestFilterContents(config)).isEqualTo("--test_filter=com.google");
+    assertThat(config.getName()).isEqualTo("Blaze test all in directory 'google'");
+    assertThat(getCommandType(config)).isEqualTo(BlazeCommandName.TEST);
+  }
+
+  @Test
+  public void testNoFilterIfDirectoryAtPackageRoot() {
+    MockBlazeProjectDataBuilder builder = MockBlazeProjectDataBuilder.builder(workspaceRoot);
+    builder.setTargetMap(
+        TargetMapBuilder.builder()
+            .addTarget(
+                TargetIdeInfo.builder()
+                    .setKind("java_test")
+                    .setLabel("//java/com/google/test:TestClass")
+                    .addSource(sourceRoot("java/com/google/test/TestClass.java"))
+                    .build())
+            .build());
+    registerProjectService(
+        BlazeProjectDataManager.class, new MockBlazeProjectDataManager(builder.build()));
+
+    PsiDirectory directory = workspace.createPsiDirectory(new WorkspacePath("java"));
+    createAndIndexFile(
+        new WorkspacePath("java/com/google/test/TestClass.java"),
+        "package com.google.test;",
+        "@org.junit.runner.RunWith(org.junit.runners.JUnit4.class)",
+        "public class TestClass {",
+        "  @org.junit.Test",
+        "  public void testMethod() {}",
+        "}");
+
+    ConfigurationContext context = createContextFromPsi(directory);
+    List<ConfigurationFromContext> configurations = context.getConfigurationsFromContext();
+    assertThat(configurations).hasSize(1);
+
+    ConfigurationFromContext fromContext = configurations.get(0);
+    assertThat(fromContext.isProducedBy(MultipleJavaClassesTestConfigurationProducer.class))
+        .isTrue();
+    assertThat(fromContext.getConfiguration()).isInstanceOf(BlazeCommandRunConfiguration.class);
+
+    BlazeCommandRunConfiguration config =
+        (BlazeCommandRunConfiguration) fromContext.getConfiguration();
+    assertThat(getTestFilterContents(config)).isNull();
+    assertThat(config.getName()).isEqualTo("Blaze test test:TestClass");
+    assertThat(getCommandType(config)).isEqualTo(BlazeCommandName.TEST);
+  }
+
+  @Test
+  public void testNotProducedFromDirectoryWithoutTests() {
+    MockBlazeProjectDataBuilder builder = MockBlazeProjectDataBuilder.builder(workspaceRoot);
+    builder.setTargetMap(
+        TargetMapBuilder.builder()
+            .addTarget(
+                TargetIdeInfo.builder()
+                    .setKind("java_test")
+                    .setLabel("//java/com/google/test:TestClass")
+                    .addSource(sourceRoot("java/com/google/test/TestClass.java"))
+                    .build())
+            .build());
+    registerProjectService(
+        BlazeProjectDataManager.class, new MockBlazeProjectDataManager(builder.build()));
+
+    PsiDirectory directory =
+        workspace.createPsiDirectory(new WorkspacePath("java/com/google/test"));
+
+    ConfigurationContext context = createContextFromPsi(directory);
+    assertThat(
+            new MultipleJavaClassesTestConfigurationProducer()
+                .createConfigurationFromContext(context))
+        .isNull();
+  }
+
+  @Test
+  public void testNotProducedFromSingleTestFile() {
+    MockBlazeProjectDataBuilder builder = MockBlazeProjectDataBuilder.builder(workspaceRoot);
+    builder.setTargetMap(
+        TargetMapBuilder.builder()
+            .addTarget(
+                TargetIdeInfo.builder()
+                    .setKind("java_test")
+                    .setLabel("//java/com/google/test:TestClass")
+                    .addSource(sourceRoot("java/com/google/test/TestClass.java"))
+                    .build())
+            .build());
+    registerProjectService(
+        BlazeProjectDataManager.class, new MockBlazeProjectDataManager(builder.build()));
+
+    workspace.createPsiDirectory(new WorkspacePath("java/com/google/test"));
+    PsiFile file =
+        createAndIndexFile(
+            new WorkspacePath("java/com/google/test/TestClass.java"),
+            "package com.google.test;",
+            "@org.junit.runner.RunWith(org.junit.runners.JUnit4.class)",
+            "public class TestClass {",
+            "  @org.junit.Test",
+            "  public void testMethod() {}",
+            "}");
+
+    ConfigurationContext context = createContextFromPsi(file);
+    assertThat(
+            new MultipleJavaClassesTestConfigurationProducer()
+                .createConfigurationFromContext(context))
+        .isNull();
+  }
+
+  @Test
+  public void testProducedFromTestFiles() {
+    MockBlazeProjectDataBuilder builder = MockBlazeProjectDataBuilder.builder(workspaceRoot);
+    builder.setTargetMap(
+        TargetMapBuilder.builder()
+            .addTarget(
+                TargetIdeInfo.builder()
+                    .setKind("java_test")
+                    .setLabel("//java/com/google/test:allTests")
+                    .addSource(sourceRoot("java/com/google/test/TestClass1.java"))
+                    .addSource(sourceRoot("java/com/google/test/TestClass2.java"))
+                    .build())
+            .build());
+    registerProjectService(
+        BlazeProjectDataManager.class, new MockBlazeProjectDataManager(builder.build()));
+
+    workspace.createPsiDirectory(new WorkspacePath("java/com/google/test"));
+    PsiFile testClass1 =
+        createAndIndexFile(
+            new WorkspacePath("java/com/google/test/TestClass1.java"),
+            "package com.google.test;",
+            "@org.junit.runner.RunWith(org.junit.runners.JUnit4.class)",
+            "public class TestClass1 {",
+            "  @org.junit.Test",
+            "  public void testMethod() {}",
+            "}");
+    PsiFile testClass2 =
+        createAndIndexFile(
+            new WorkspacePath("java/com/google/test/TestClass2.java"),
+            "package com.google.test;",
+            "@org.junit.runner.RunWith(org.junit.runners.JUnit4.class)",
+            "public class TestClass2 {",
+            "  @org.junit.Test",
+            "  public void testMethod() {}",
+            "}");
+
+    ConfigurationContext context =
+        createContextFromMultipleElements(new PsiElement[] {testClass1, testClass2});
+    ConfigurationFromContext fromContext =
+        new MultipleJavaClassesTestConfigurationProducer().createConfigurationFromContext(context);
+    assertThat(fromContext).isNotNull();
+    assertThat(fromContext.getConfiguration()).isInstanceOf(BlazeCommandRunConfiguration.class);
+
+    BlazeCommandRunConfiguration config =
+        (BlazeCommandRunConfiguration) fromContext.getConfiguration();
+    assertThat(config.getTarget())
+        .isEqualTo(TargetExpression.fromString("//java/com/google/test:allTests"));
+    assertThat(getTestFilterContents(config))
+        .isEqualTo("--test_filter=com.google.test.TestClass1#|com.google.test.TestClass2#");
+    assertThat(config.getName()).isEqualTo("Blaze test TestClass1 and 1 others");
+    assertThat(getCommandType(config)).isEqualTo(BlazeCommandName.TEST);
+  }
+
+  @Test
+  public void testNotProducedFromTestFilesInDifferentTestTargets() {
+    MockBlazeProjectDataBuilder builder = MockBlazeProjectDataBuilder.builder(workspaceRoot);
+    builder.setTargetMap(
+        TargetMapBuilder.builder()
+            .addTarget(
+                TargetIdeInfo.builder()
+                    .setKind("java_test")
+                    .setLabel("//java/com/google/test:TestClass1")
+                    .addSource(sourceRoot("java/com/google/test/TestClass1.java"))
+                    .build())
+            .addTarget(
+                TargetIdeInfo.builder()
+                    .setKind("java_test")
+                    .setLabel("//java/com/google/test:TestClass2")
+                    .addSource(sourceRoot("java/com/google/test/TestClass2.java"))
+                    .build())
+            .build());
+    registerProjectService(
+        BlazeProjectDataManager.class, new MockBlazeProjectDataManager(builder.build()));
+
+    workspace.createPsiDirectory(new WorkspacePath("java/com/google/test"));
+    PsiFile testClass1 =
+        createAndIndexFile(
+            new WorkspacePath("java/com/google/test/TestClass1.java"),
+            "package com.google.test;",
+            "@org.junit.runner.RunWith(org.junit.runners.JUnit4.class)",
+            "public class TestClass1 {",
+            "  @org.junit.Test",
+            "  public void testMethod() {}",
+            "}");
+    PsiFile testClass2 =
+        createAndIndexFile(
+            new WorkspacePath("java/com/google/test/TestClass2.java"),
+            "package com.google.test;",
+            "@org.junit.runner.RunWith(org.junit.runners.JUnit4.class)",
+            "public class TestClass2 {",
+            "  @org.junit.Test",
+            "  public void testMethod() {}",
+            "}");
+
+    ConfigurationContext context =
+        createContextFromMultipleElements(new PsiElement[] {testClass1, testClass2});
+    assertThat(
+            new MultipleJavaClassesTestConfigurationProducer()
+                .createConfigurationFromContext(context))
+        .isNull();
+  }
+
+  @Test
+  public void testNotProducedFromNonTestFiles() {
+    MockBlazeProjectDataBuilder builder = MockBlazeProjectDataBuilder.builder(workspaceRoot);
+    builder.setTargetMap(
+        TargetMapBuilder.builder()
+            .addTarget(
+                TargetIdeInfo.builder()
+                    .setKind("java_test")
+                    .setLabel("//java/com/google/test:allTests")
+                    .addSource(sourceRoot("java/com/google/test/NonTestClass1.java"))
+                    .addSource(sourceRoot("java/com/google/test/NonTestClass2.java"))
+                    .build())
+            .build());
+    registerProjectService(
+        BlazeProjectDataManager.class, new MockBlazeProjectDataManager(builder.build()));
+
+    workspace.createPsiDirectory(new WorkspacePath("java/com/google/test"));
+    PsiFile testClass1 =
+        createAndIndexFile(
+            new WorkspacePath("java/com/google/test/NonTestClass1.java"),
+            "package com.google.test;",
+            "public class NonTestClass1 {");
+    PsiFile testClass2 =
+        createAndIndexFile(
+            new WorkspacePath("java/com/google/test/NonTestClass2.java"),
+            "package com.google.test;",
+            "public class NonTestClass2 {}");
+
+    ConfigurationContext context =
+        createContextFromMultipleElements(new PsiElement[] {testClass1, testClass2});
+    ConfigurationFromContext fromContext =
+        new MultipleJavaClassesTestConfigurationProducer().createConfigurationFromContext(context);
+    assertThat(fromContext).isNull();
+  }
+}
diff --git a/java/tests/integrationtests/com/google/idea/blaze/java/sync/projectstructure/JavaSourceFolderProviderTest.java b/java/tests/integrationtests/com/google/idea/blaze/java/sync/projectstructure/JavaSourceFolderProviderTest.java
index bd15bde..2412829 100644
--- a/java/tests/integrationtests/com/google/idea/blaze/java/sync/projectstructure/JavaSourceFolderProviderTest.java
+++ b/java/tests/integrationtests/com/google/idea/blaze/java/sync/projectstructure/JavaSourceFolderProviderTest.java
@@ -140,6 +140,42 @@
     assertThat(testSourceChild.getPackagePrefix()).isEqualTo("apps.tests.model");
   }
 
+  @Test
+  public void testRelativePackagePrefixWithoutParentPrefix() {
+    ImmutableList<BlazeContentEntry> contentEntries =
+        ImmutableList.of(
+            BlazeContentEntry.builder("/src/workspace/java")
+                .addSource(
+                    BlazeSourceDirectory.builder("/src/workspace/java")
+                        .setPackagePrefix("")
+                        .build())
+                .build());
+
+    JavaSourceFolderProvider provider =
+        new JavaSourceFolderProvider(
+            new BlazeJavaSyncData(
+                new BlazeJavaImportResult(
+                    contentEntries, ImmutableMap.of(), ImmutableList.of(), ImmutableSet.of(), null),
+                new GlobSet(ImmutableList.of())));
+
+    VirtualFile root = workspace.createDirectory(new WorkspacePath("java"));
+    ContentEntry contentEntry = getContentEntry(root);
+
+    ImmutableMap<File, SourceFolder> sourceFolders = provider.initializeSourceFolders(contentEntry);
+    assertThat(sourceFolders).hasSize(1);
+
+    VirtualFile testRoot = workspace.createDirectory(new WorkspacePath("java/apps/tests"));
+
+    SourceFolder testSourceChild =
+        provider.setSourceFolderForLocation(
+            contentEntry,
+            sourceFolders.get(new File(root.getPath())),
+            new File(testRoot.getPath()),
+            true);
+    assertThat(testSourceChild.isTestSource()).isTrue();
+    assertThat(testSourceChild.getPackagePrefix()).isEqualTo("apps.tests");
+  }
+
   private ContentEntry getContentEntry(VirtualFile root) {
     return ModuleRootManager.getInstance(testFixture.getModule())
         .getModifiableModel()
diff --git a/java/tests/unittests/com/google/idea/blaze/java/run/BlazeJavaRunProfileStateTest.java b/java/tests/unittests/com/google/idea/blaze/java/run/BlazeJavaRunProfileStateTest.java
index 622105e..612b946 100644
--- a/java/tests/unittests/com/google/idea/blaze/java/run/BlazeJavaRunProfileStateTest.java
+++ b/java/tests/unittests/com/google/idea/blaze/java/run/BlazeJavaRunProfileStateTest.java
@@ -16,10 +16,13 @@
 package com.google.idea.blaze.java.run;
 
 import static com.google.common.truth.Truth.assertThat;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
 
 import com.google.common.base.Predicate;
 import com.google.common.collect.ImmutableList;
 import com.google.idea.blaze.base.BlazeTestCase;
+import com.google.idea.blaze.base.bazel.BuildSystemProvider;
 import com.google.idea.blaze.base.command.BlazeCommandName;
 import com.google.idea.blaze.base.command.BlazeFlags;
 import com.google.idea.blaze.base.command.BuildFlagsProvider;
@@ -53,15 +56,14 @@
 public class BlazeJavaRunProfileStateTest extends BlazeTestCase {
 
   private static final BlazeImportSettings DUMMY_IMPORT_SETTINGS =
-      new BlazeImportSettings("", "", "", "", "", BuildSystem.Blaze);
+      new BlazeImportSettings("", "", "", "", BuildSystem.Blaze);
 
   private BlazeCommandRunConfiguration configuration;
 
   @Override
   protected void initTest(
       @NotNull Container applicationServices, @NotNull Container projectServices) {
-    projectServices.register(
-        BlazeImportSettingsManager.class, new BlazeImportSettingsManager(project));
+    projectServices.register(BlazeImportSettingsManager.class, new BlazeImportSettingsManager());
     BlazeImportSettingsManager.getInstance(getProject()).setImportSettings(DUMMY_IMPORT_SETTINGS);
 
     ExperimentService experimentService = new MockExperimentService();
@@ -75,6 +77,11 @@
             BlazeCommandRunConfigurationHandlerProvider.EP_NAME,
             BlazeCommandRunConfigurationHandlerProvider.class);
     handlerProviderEp.registerExtension(new BlazeCommandGenericRunConfigurationHandlerProvider());
+    ExtensionPointImpl<BuildSystemProvider> buildSystemProviderExtensionPoint =
+        registerExtensionPoint(BuildSystemProvider.EP_NAME, BuildSystemProvider.class);
+    BuildSystemProvider buildSystemProvider = mock(BuildSystemProvider.class);
+    when(buildSystemProvider.getBinaryPath()).thenReturn("/usr/bin/blaze");
+    buildSystemProviderExtensionPoint.registerExtension(buildSystemProvider);
 
     configuration =
         new BlazeCommandRunConfigurationType().getFactory().createTemplateConfiguration(project);
@@ -82,18 +89,18 @@
 
   @Test
   public void flagsShouldBeAppendedIfPresent() {
-    configuration.setTarget(new Label("//label:rule"));
+    configuration.setTarget(Label.create("//label:rule"));
     BlazeCommandRunConfigurationCommonState handlerState =
         (BlazeCommandRunConfigurationCommonState) configuration.getHandler().getState();
-    handlerState.setCommand(BlazeCommandName.fromString("command"));
-    handlerState.setBlazeFlags(ImmutableList.of("--flag1", "--flag2"));
+    handlerState.getCommandState().setCommand(BlazeCommandName.fromString("command"));
+    handlerState.getBlazeFlagsState().setRawFlags(ImmutableList.of("--flag1", "--flag2"));
     assertThat(
             BlazeJavaRunProfileState.getBlazeCommand(
                     project,
                     configuration,
                     ProjectViewSet.builder().build(),
                     ImmutableList.of(),
-                    false /* debug */)
+                    /* debug */ false)
                 .toList())
         .isEqualTo(
             ImmutableList.of(
@@ -102,24 +109,23 @@
                 BlazeFlags.getToolTagFlag(),
                 "--flag1",
                 "--flag2",
-                "--test_output=streamed",
                 "--",
                 "//label:rule"));
   }
 
   @Test
   public void debugFlagShouldBeIncludedForJavaTest() {
-    configuration.setTarget(new Label("//label:rule"));
+    configuration.setTarget(Label.create("//label:rule"));
     BlazeCommandRunConfigurationCommonState handlerState =
         (BlazeCommandRunConfigurationCommonState) configuration.getHandler().getState();
-    handlerState.setCommand(BlazeCommandName.fromString("command"));
+    handlerState.getCommandState().setCommand(BlazeCommandName.fromString("command"));
     assertThat(
             BlazeJavaRunProfileState.getBlazeCommand(
                     project,
                     configuration,
                     ProjectViewSet.builder().build(),
                     ImmutableList.of(),
-                    true /* debug */)
+                    /* debug */ true)
                 .toList())
         .isEqualTo(
             ImmutableList.of(
@@ -133,17 +139,17 @@
 
   @Test
   public void debugFlagShouldBeIncludedForJavaBinary() {
-    configuration.setTarget(new Label("//label:java_binary_rule"));
+    configuration.setTarget(Label.create("//label:java_binary_rule"));
     BlazeCommandRunConfigurationCommonState handlerState =
         (BlazeCommandRunConfigurationCommonState) configuration.getHandler().getState();
-    handlerState.setCommand(BlazeCommandName.fromString("command"));
+    handlerState.getCommandState().setCommand(BlazeCommandName.fromString("command"));
     assertThat(
             BlazeJavaRunProfileState.getBlazeCommand(
                     project,
                     configuration,
                     ProjectViewSet.builder().build(),
                     ImmutableList.of(),
-                    true /* debug */)
+                    /* debug */ true)
                 .toList())
         .isEqualTo(
             ImmutableList.of(
diff --git a/java/tests/unittests/com/google/idea/blaze/java/sync/importer/BlazeJavaWorkspaceImporterTest.java b/java/tests/unittests/com/google/idea/blaze/java/sync/importer/BlazeJavaWorkspaceImporterTest.java
index ad731d7..c69b66b 100644
--- a/java/tests/unittests/com/google/idea/blaze/java/sync/importer/BlazeJavaWorkspaceImporterTest.java
+++ b/java/tests/unittests/com/google/idea/blaze/java/sync/importer/BlazeJavaWorkspaceImporterTest.java
@@ -16,9 +16,7 @@
 package com.google.idea.blaze.java.sync.importer;
 
 import static com.google.common.truth.Truth.assertThat;
-import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertNotNull;
-import static org.junit.Assert.assertTrue;
 
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableSet;
@@ -70,6 +68,7 @@
 import com.google.idea.blaze.java.sync.model.BlazeJarLibrary;
 import com.google.idea.blaze.java.sync.model.BlazeJavaImportResult;
 import com.google.idea.blaze.java.sync.model.BlazeSourceDirectory;
+import com.google.idea.blaze.java.sync.source.JavaLikeLanguage;
 import com.google.idea.blaze.java.sync.source.JavaSourcePackageReader;
 import com.google.idea.blaze.java.sync.source.PackageManifestReader;
 import com.google.idea.blaze.java.sync.source.SourceArtifact;
@@ -83,8 +82,8 @@
 import java.util.Map;
 import java.util.function.Predicate;
 import java.util.stream.Collectors;
+import javax.annotation.Nullable;
 import org.jetbrains.annotations.NotNull;
-import org.jetbrains.annotations.Nullable;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.junit.runners.JUnit4;
@@ -94,18 +93,17 @@
 public class BlazeJavaWorkspaceImporterTest extends BlazeTestCase {
 
   private static final String FAKE_WORKSPACE_ROOT = "/root";
-  private WorkspaceRoot workspaceRoot = new WorkspaceRoot(new File(FAKE_WORKSPACE_ROOT));
+  private final WorkspaceRoot workspaceRoot = new WorkspaceRoot(new File(FAKE_WORKSPACE_ROOT));
 
   private static final String FAKE_GEN_ROOT_EXECUTION_PATH_FRAGMENT =
       "blaze-out/gcc-4.X.Y-crosstool-v17-hybrid-grtev3-k8-fastbuild/bin";
 
-
   private static final ArtifactLocationDecoder FAKE_ARTIFACT_DECODER =
       (ArtifactLocationDecoder)
           artifactLocation -> new File("/", artifactLocation.getRelativePath());
 
   private static final BlazeImportSettings DUMMY_IMPORT_SETTINGS =
-      new BlazeImportSettings("", "", "", "", "", BuildSystem.Blaze);
+      new BlazeImportSettings("", "", "", "", BuildSystem.Blaze);
   private ExtensionPointImpl<BlazeJavaSyncAugmenter> augmenters;
 
   private static class JdepsMock implements JdepsMap {
@@ -124,7 +122,7 @@
   }
 
   private BlazeContext context;
-  private ErrorCollector errorCollector = new ErrorCollector();
+  private final ErrorCollector errorCollector = new ErrorCollector();
   private final JdepsMock jdepsMap = new JdepsMock();
   private JavaWorkingSet workingSet = null;
   private final WorkspaceLanguageSettings workspaceLanguageSettings =
@@ -132,6 +130,7 @@
   private MockExperimentService experimentService;
 
   @Override
+  @SuppressWarnings("FunctionalInterfaceClash") // False positive on getDeclaredPackageOfJavaFile.
   protected void initTest(
       @NotNull Container applicationServices, @NotNull Container projectServices) {
     experimentService = new MockExperimentService();
@@ -139,8 +138,7 @@
 
     BlazeExecutor blazeExecutor = new MockBlazeExecutor();
     applicationServices.register(BlazeExecutor.class, blazeExecutor);
-    projectServices.register(
-        BlazeImportSettingsManager.class, new BlazeImportSettingsManager(project));
+    projectServices.register(BlazeImportSettingsManager.class, new BlazeImportSettingsManager());
     BlazeImportSettingsManager.getInstance(getProject()).setImportSettings(DUMMY_IMPORT_SETTINGS);
 
     // will silently fall back to FilePathJavaPackageReader
@@ -164,6 +162,9 @@
 
     augmenters =
         registerExtensionPoint(BlazeJavaSyncAugmenter.EP_NAME, BlazeJavaSyncAugmenter.class);
+
+    registerExtensionPoint(JavaLikeLanguage.EP_NAME, JavaLikeLanguage.class)
+        .registerExtension(new JavaLikeLanguage.Java());
   }
 
   BlazeJavaImportResult importWorkspace(
@@ -195,7 +196,7 @@
     BlazeJavaImportResult result =
         importWorkspace(workspaceRoot, TargetMapBuilder.builder(), ProjectView.builder().build());
     errorCollector.assertNoIssues();
-    assertTrue(result.contentEntries.isEmpty());
+    assertThat(result.contentEntries).isEmpty();
   }
 
   @Test
@@ -233,10 +234,10 @@
     BlazeJavaImportResult result = importWorkspace(workspaceRoot, targetMapBuilder, projectView);
     errorCollector.assertNoIssues();
 
-    assertEquals(1, result.buildOutputJars.size());
+    assertThat(result.buildOutputJars).hasSize(1);
     ArtifactLocation compilerOutputLib = result.buildOutputJars.iterator().next();
     assertNotNull(compilerOutputLib);
-    assertTrue(compilerOutputLib.relativePath.endsWith("example_debug.jar"));
+    assertThat(compilerOutputLib.relativePath).endsWith("example_debug.jar");
 
     assertThat(result.contentEntries)
         .containsExactly(
@@ -668,7 +669,7 @@
 
     BlazeJavaImportResult result = importWorkspace(workspaceRoot, response, projectView);
     errorCollector.assertNoIssues();
-    assertEquals(1, result.libraries.size());
+    assertThat(result.libraries).hasSize(1);
   }
 
   @Test
@@ -713,7 +714,7 @@
     BlazeJavaImportResult result = importWorkspace(workspaceRoot, response, projectView);
     errorCollector.assertNoIssues();
 
-    assertEquals(1, result.libraries.size());
+    assertThat(result.libraries).hasSize(1);
   }
 
   @Test
@@ -761,7 +762,7 @@
 
     BlazeJavaImportResult result = importWorkspace(workspaceRoot, response, projectView);
     errorCollector.assertNoIssues();
-    assertEquals(1, result.libraries.size()); // The libraries were merged
+    assertThat(result.libraries).hasSize(1); // The libraries were merged
   }
 
   @Test
@@ -893,7 +894,7 @@
                     .add(DirectoryEntry.include(new WorkspacePath("import"))))
             .add(
                 ListSection.builder(ImportTargetOutputSection.KEY)
-                    .add(new Label("//import:import")))
+                    .add(Label.create("//import:import")))
             .build();
 
     TargetMapBuilder response =
@@ -989,7 +990,7 @@
             .build();
     TargetMapBuilder targetMapBuilder = targetMapForJdepsSuite();
     jdepsMap.put(
-        TargetKey.forPlainTarget(new Label("//java/apps/example:example_debug")),
+        TargetKey.forPlainTarget(Label.create("//java/apps/example:example_debug")),
         Lists.newArrayList(jdepsPath("thirdparty/a.jar"), jdepsPath("thirdparty/c.jar")));
 
     BlazeJavaImportResult result = importWorkspace(workspaceRoot, targetMapBuilder, projectView);
@@ -1073,7 +1074,7 @@
                     .add(DirectoryEntry.include(new WorkspacePath("java/apps/example"))))
             .add(
                 ListSection.builder(ExcludeTargetSection.KEY)
-                    .add(new Label("//java/apps/example:example")))
+                    .add(Label.create("//java/apps/example:example")))
             .build();
 
     TargetMapBuilder targetMapBuilder =
@@ -1220,7 +1221,7 @@
 
     // First test - make sure that jdeps is working
     jdepsMap.put(
-        TargetKey.forPlainTarget(new Label("//java/example:liba")),
+        TargetKey.forPlainTarget(Label.create("//java/example:liba")),
         Lists.newArrayList(jdepsPath("thirdparty/proto/a/liba-ijar.jar")));
     BlazeJavaImportResult result = importWorkspace(workspaceRoot, targetMapBuilder, projectView);
     errorCollector.assertNoIssues();
@@ -1332,11 +1333,10 @@
               TargetIdeInfo target,
               Collection<BlazeJarLibrary> jars,
               Collection<BlazeJarLibrary> genJars) {
-            if (target.key.label.equals(new Label("//java/example:source"))) {
+            if (target.key.label.equals(Label.create("//java/example:source"))) {
               jars.add(
                   new BlazeJarLibrary(
-                      LibraryArtifact.builder().setInterfaceJar(gen("source.jar")).build(),
-                      target.key));
+                      LibraryArtifact.builder().setInterfaceJar(gen("source.jar")).build()));
             }
           }
         });
@@ -1394,11 +1394,8 @@
     return null;
   }
 
-  private ArtifactLocation source(String relativePath) {
-    return ArtifactLocation.builder()
-        .setRelativePath(relativePath)
-        .setIsSource(true)
-        .build();
+  private static ArtifactLocation source(String relativePath) {
+    return ArtifactLocation.builder().setRelativePath(relativePath).setIsSource(true).build();
   }
 
   private static ArtifactLocation gen(String relativePath) {
diff --git a/java/tests/unittests/com/google/idea/blaze/java/sync/source/SourceDirectoryCalculatorTest.java b/java/tests/unittests/com/google/idea/blaze/java/sync/source/SourceDirectoryCalculatorTest.java
index 25fac98..0538155 100644
--- a/java/tests/unittests/com/google/idea/blaze/java/sync/source/SourceDirectoryCalculatorTest.java
+++ b/java/tests/unittests/com/google/idea/blaze/java/sync/source/SourceDirectoryCalculatorTest.java
@@ -24,11 +24,11 @@
 import com.google.idea.blaze.base.BlazeTestCase;
 import com.google.idea.blaze.base.async.executor.BlazeExecutor;
 import com.google.idea.blaze.base.async.executor.MockBlazeExecutor;
+import com.google.idea.blaze.base.command.info.BlazeInfo;
 import com.google.idea.blaze.base.ideinfo.ArtifactLocation;
 import com.google.idea.blaze.base.ideinfo.TargetKey;
 import com.google.idea.blaze.base.io.FileAttributeProvider;
 import com.google.idea.blaze.base.io.InputStreamProvider;
-import com.google.idea.blaze.base.model.primitives.ExecutionRootPath;
 import com.google.idea.blaze.base.model.primitives.Label;
 import com.google.idea.blaze.base.model.primitives.WorkspacePath;
 import com.google.idea.blaze.base.model.primitives.WorkspaceRoot;
@@ -39,7 +39,6 @@
 import com.google.idea.blaze.base.scope.output.IssueOutput;
 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.WorkspacePathResolverImpl;
 import com.google.idea.blaze.java.sync.model.BlazeContentEntry;
 import com.google.idea.blaze.java.sync.model.BlazeSourceDirectory;
@@ -66,17 +65,17 @@
 public class SourceDirectoryCalculatorTest extends BlazeTestCase {
 
   private static final ImmutableMap<TargetKey, ArtifactLocation> NO_MANIFESTS = ImmutableMap.of();
-  private static final Label LABEL = new Label("//fake:label");
+  private static final Label LABEL = Label.create("//fake:label");
 
   private MockInputStreamProvider mockInputStreamProvider;
   private SourceDirectoryCalculator sourceDirectoryCalculator;
 
-  private BlazeContext context = new BlazeContext();
-  private ErrorCollector issues = new ErrorCollector();
+  private final BlazeContext context = new BlazeContext();
+  private final ErrorCollector issues = new ErrorCollector();
   private MockExperimentService experimentService;
 
-  private WorkspaceRoot workspaceRoot = new WorkspaceRoot(new File("/root"));
-  private ArtifactLocationDecoder decoder =
+  private final WorkspaceRoot workspaceRoot = new WorkspaceRoot(new File("/root"));
+  private final ArtifactLocationDecoder decoder =
       (ArtifactLocationDecoder)
           artifactLocation -> new File("/root", artifactLocation.getRelativePath());
 
@@ -101,6 +100,9 @@
     applicationServices.register(ExperimentService.class, experimentService);
 
     applicationServices.register(PrefetchService.class, new MockPrefetchService());
+
+    registerExtensionPoint(JavaLikeLanguage.EP_NAME, JavaLikeLanguage.class)
+        .registerExtension(new JavaLikeLanguage.Java());
   }
 
   @Test
@@ -894,16 +896,14 @@
                 .build()),
         ImmutableList.of("com.google"));
     ImmutableMap<TargetKey, ArtifactLocation> manifests =
-        ImmutableMap.<TargetKey, ArtifactLocation>builder()
-            .put(
-                TargetKey.forPlainTarget(LABEL),
-                ArtifactLocation.builder()
-                    .setRelativePath("java/com/test.manifest")
-                    .setIsSource(true)
-                    .build())
-            .build();
+        ImmutableMap.of(
+            TargetKey.forPlainTarget(LABEL),
+            ArtifactLocation.builder()
+                .setRelativePath("java/com/test.manifest")
+                .setIsSource(true)
+                .build());
     Map<TargetKey, Map<ArtifactLocation, String>> manifestMap =
-        readPackageManifestFiles(manifests, getDecoder("/root"));
+        readPackageManifestFiles(manifests, getDecoder());
 
     assertThat(manifestMap.get(TargetKey.forPlainTarget(LABEL)))
         .containsEntry(
@@ -921,16 +921,14 @@
         ImmutableList.of("java/com/google/Bla.java"),
         ImmutableList.of("com.google"));
     ImmutableMap<TargetKey, ArtifactLocation> manifests =
-        ImmutableMap.<TargetKey, ArtifactLocation>builder()
-            .put(
-                TargetKey.forPlainTarget(LABEL),
-                ArtifactLocation.builder()
-                    .setRelativePath("java/com/test.manifest")
-                    .setIsSource(true)
-                    .build())
-            .build();
+        ImmutableMap.of(
+            TargetKey.forPlainTarget(LABEL),
+            ArtifactLocation.builder()
+                .setRelativePath("java/com/test.manifest")
+                .setIsSource(true)
+                .build());
     Map<TargetKey, Map<ArtifactLocation, String>> manifestMap =
-        readPackageManifestFiles(manifests, getDecoder("/root"));
+        readPackageManifestFiles(manifests, getDecoder());
 
     assertThat(manifestMap.get(TargetKey.forPlainTarget(LABEL)))
         .containsEntry(
@@ -954,38 +952,38 @@
     ImmutableMap<TargetKey, ArtifactLocation> manifests =
         ImmutableMap.<TargetKey, ArtifactLocation>builder()
             .put(
-                TargetKey.forPlainTarget(new Label("//a:a")),
+                TargetKey.forPlainTarget(Label.create("//a:a")),
                 ArtifactLocation.builder()
                     .setRelativePath("java/com/test.manifest")
                     .setIsSource(true)
                     .build())
             .put(
-                TargetKey.forPlainTarget(new Label("//b:b")),
+                TargetKey.forPlainTarget(Label.create("//b:b")),
                 ArtifactLocation.builder()
                     .setRelativePath("java/com/test2.manifest")
                     .setIsSource(true)
                     .build())
             .build();
     Map<TargetKey, Map<ArtifactLocation, String>> manifestMap =
-        readPackageManifestFiles(manifests, getDecoder("/root"));
+        readPackageManifestFiles(manifests, getDecoder());
 
     assertThat(manifestMap).hasSize(2);
 
-    assertThat(manifestMap.get(TargetKey.forPlainTarget(new Label("//a:a"))))
+    assertThat(manifestMap.get(TargetKey.forPlainTarget(Label.create("//a:a"))))
         .containsEntry(
             ArtifactLocation.builder()
                 .setRelativePath("java/com/google/Bla.java")
                 .setIsSource(true)
                 .build(),
             "com.google");
-    assertThat(manifestMap.get(TargetKey.forPlainTarget(new Label("//a:a"))))
+    assertThat(manifestMap.get(TargetKey.forPlainTarget(Label.create("//a:a"))))
         .containsEntry(
             ArtifactLocation.builder()
                 .setRelativePath("java/com/google/Foo.java")
                 .setIsSource(true)
                 .build(),
             "com.google.subpackage");
-    assertThat(manifestMap.get(TargetKey.forPlainTarget(new Label("//b:b"))))
+    assertThat(manifestMap.get(TargetKey.forPlainTarget(Label.create("//b:b"))))
         .containsEntry(
             ArtifactLocation.builder()
                 .setRelativePath("java/com/google/other/Temp.java")
@@ -1006,14 +1004,12 @@
         "package com.google.different;\n public class Bla {}");
 
     ImmutableMap<TargetKey, ArtifactLocation> manifests =
-        ImmutableMap.<TargetKey, ArtifactLocation>builder()
-            .put(
-                TargetKey.forPlainTarget(LABEL),
-                ArtifactLocation.builder()
-                    .setRelativePath("java/com/test.manifest")
-                    .setIsSource(true)
-                    .build())
-            .build();
+        ImmutableMap.of(
+            TargetKey.forPlainTarget(LABEL),
+            ArtifactLocation.builder()
+                .setRelativePath("java/com/test.manifest")
+                .setIsSource(true)
+                .build());
 
     List<SourceArtifact> sourceArtifacts =
         ImmutableList.of(
@@ -1041,7 +1037,7 @@
             project,
             context,
             workspaceRoot,
-            getDecoder("/root"),
+            getDecoder(),
             ImmutableList.of(new WorkspacePath("java/com/google")),
             sourceArtifacts,
             manifests);
@@ -1093,23 +1089,18 @@
     mockInputStreamProvider.addFile(manifestPath, manifest.build().toByteArray());
   }
 
-  private static ArtifactLocationDecoder getDecoder(String rootPath) {
-    File root = new File(rootPath);
+  private static ArtifactLocationDecoder getDecoder() {
+    File root = new File("/root");
     WorkspaceRoot workspaceRoot = new WorkspaceRoot(root);
-    BlazeRoots roots =
-        new BlazeRoots(
-            root,
-            ImmutableList.of(root),
-            new ExecutionRootPath("out/crosstool/bin"),
-            new ExecutionRootPath("out/crosstool/gen"),
-            null);
-    return new ArtifactLocationDecoderImpl(
-        roots, new WorkspacePathResolverImpl(workspaceRoot, roots));
+    BlazeInfo roots =
+        BlazeInfo.createMockBlazeInfo(
+            "/", "/root", "/root/out/crosstool/bin", "/root/out/crosstool/gen");
+    return new ArtifactLocationDecoderImpl(roots, new WorkspacePathResolverImpl(workspaceRoot));
   }
 
   private static class MockInputStreamProvider implements InputStreamProvider {
 
-    private final Map<String, InputStream> javaFiles = new HashMap<String, InputStream>();
+    private final Map<String, InputStream> javaFiles = new HashMap<>();
 
     public MockInputStreamProvider addFile(String filePath, String javaSrc) {
       try {
@@ -1140,7 +1131,7 @@
       Map<TargetKey, ArtifactLocation> manifests, ArtifactLocationDecoder decoder) {
     return PackageManifestReader.getInstance()
         .readPackageManifestFiles(
-            project, context, decoder, manifests, MoreExecutors.sameThreadExecutor());
+            project, context, decoder, manifests, MoreExecutors.newDirectExecutorService());
   }
 
   static class MockFileAttributeProvider extends FileAttributeProvider {
diff --git a/plugin_dev/BUILD b/plugin_dev/BUILD
index 1c56953..1586378 100644
--- a/plugin_dev/BUILD
+++ b/plugin_dev/BUILD
@@ -9,6 +9,7 @@
         "//intellij_platform_sdk:devkit",
         "//intellij_platform_sdk:plugin_api",
         "//java",
+        "//proto:proto_deps",
         "//sdkcompat",
         "@jsr305_annotations//jar",
     ],
diff --git a/plugin_dev/src/com/google/idea/blaze/plugin/IntellijPluginRule.java b/plugin_dev/src/com/google/idea/blaze/plugin/IntellijPluginRule.java
index 3947b69..608c452 100644
--- a/plugin_dev/src/com/google/idea/blaze/plugin/IntellijPluginRule.java
+++ b/plugin_dev/src/com/google/idea/blaze/plugin/IntellijPluginRule.java
@@ -22,21 +22,14 @@
 public class IntellijPluginRule {
 
   public static final String TARGET_TAG_IJ_PLUGIN = "intellij-plugin";
-  public static final String TARGET_TAG_IJ_PLUGIN_BUNDLE = "intellij-plugin-bundle";
 
   public static boolean isPluginTarget(TargetIdeInfo target) {
     return isIntellijPluginDebugTarget(target)
-        || isPluginBundle(target)
         || isSinglePluginTarget(target);
   }
 
   public static boolean isIntellijPluginDebugTarget(TargetIdeInfo target) {
-    return target.intellijPluginDeployInfo != null;
-  }
-
-  public static boolean isPluginBundle(TargetIdeInfo target) {
-    return target.kindIsOneOf(Kind.JAVA_LIBRARY)
-        && target.tags.contains(TARGET_TAG_IJ_PLUGIN_BUNDLE);
+    return target.kind == Kind.INTELLIJ_PLUGIN_DEBUG_TARGET;
   }
 
   public static boolean isSinglePluginTarget(TargetIdeInfo target) {
diff --git a/plugin_dev/src/com/google/idea/blaze/plugin/run/BlazeIntellijPluginConfiguration.java b/plugin_dev/src/com/google/idea/blaze/plugin/run/BlazeIntellijPluginConfiguration.java
index 528cac4..3621e36 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
@@ -20,13 +20,9 @@
 import com.google.common.base.Strings;
 import com.google.common.collect.Lists;
 import com.google.common.collect.Ordering;
-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.ideinfo.TargetIdeInfo;
 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.run.BlazeConfigurationNameBuilder;
 import com.google.idea.blaze.base.run.BlazeRunConfiguration;
 import com.google.idea.blaze.base.run.state.RunConfigurationFlagsState;
@@ -194,6 +190,7 @@
     final BlazeIntellijPluginDeployer deployer =
         new BlazeIntellijPluginDeployer(getProject(), sandboxHome, buildNumber);
     deployer.addTarget(getTarget());
+    env.putUserData(BlazeIntellijPluginDeployer.USER_DATA_KEY, deployer);
 
     // copy license from running instance of idea
     IdeaJdkHelper.copyIDEALicense(sandboxHome);
@@ -332,8 +329,8 @@
     final BlazeIntellijPluginConfiguration configuration =
         (BlazeIntellijPluginConfiguration) super.clone();
     configuration.target = target;
-    configuration.blazeFlags.setFlags(blazeFlags.getFlags());
-    configuration.exeFlags.setFlags(exeFlags.getFlags());
+    configuration.blazeFlags.setRawFlags(blazeFlags.getRawFlags());
+    configuration.exeFlags.setRawFlags(exeFlags.getRawFlags());
     configuration.pluginSdk = pluginSdk;
     configuration.vmParameters = vmParameters;
     configuration.programParameters = programParameters;
@@ -341,14 +338,12 @@
     return configuration;
   }
 
-  protected BlazeCommand buildBlazeCommand(Project project, ProjectViewSet projectViewSet) {
-    BlazeCommand.Builder command =
-        BlazeCommand.builder(Blaze.getBuildSystem(getProject()), BlazeCommandName.BUILD)
-            .addTargets(getTarget())
-            .addBlazeFlags(BlazeFlags.buildFlags(project, projectViewSet))
-            .addBlazeFlags(blazeFlags.getFlags())
-            .addExeFlags(exeFlags.getFlags());
-    return command.build();
+  RunConfigurationFlagsState getBlazeFlagsState() {
+    return blazeFlags;
+  }
+
+  RunConfigurationFlagsState getExeFlagsState() {
+    return exeFlags;
   }
 
   @Override
diff --git a/plugin_dev/src/com/google/idea/blaze/plugin/run/BlazeIntellijPluginDeployer.java b/plugin_dev/src/com/google/idea/blaze/plugin/run/BlazeIntellijPluginDeployer.java
index c06caf2..2c366f9 100644
--- a/plugin_dev/src/com/google/idea/blaze/plugin/run/BlazeIntellijPluginDeployer.java
+++ b/plugin_dev/src/com/google/idea/blaze/plugin/run/BlazeIntellijPluginDeployer.java
@@ -15,14 +15,13 @@
  */
 package com.google.idea.blaze.plugin.run;
 
+import static java.nio.charset.StandardCharsets.UTF_8;
+
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableMap;
 import com.google.common.collect.Lists;
 import com.google.common.collect.Maps;
-import com.google.idea.blaze.base.ideinfo.Dependency;
-import com.google.idea.blaze.base.ideinfo.Dependency.DependencyType;
-import com.google.idea.blaze.base.ideinfo.IntellijPluginDeployInfo;
-import com.google.idea.blaze.base.ideinfo.IntellijPluginDeployInfo.IntellijPluginDeployFile;
+import com.google.idea.blaze.base.command.buildresult.BuildResultHelper;
 import com.google.idea.blaze.base.ideinfo.JavaIdeInfo;
 import com.google.idea.blaze.base.ideinfo.LibraryArtifact;
 import com.google.idea.blaze.base.ideinfo.TargetIdeInfo;
@@ -31,29 +30,43 @@
 import com.google.idea.blaze.base.model.BlazeProjectData;
 import com.google.idea.blaze.base.model.primitives.Label;
 import com.google.idea.blaze.base.sync.data.BlazeProjectDataManager;
-import com.google.idea.blaze.base.sync.workspace.ArtifactLocationDecoder;
 import com.google.idea.blaze.plugin.IntellijPluginRule;
+import com.google.repackaged.devtools.intellij.plugin.IntellijPluginTargetDeployInfo.IntellijPluginDeployFile;
+import com.google.repackaged.devtools.intellij.plugin.IntellijPluginTargetDeployInfo.IntellijPluginDeployInfo;
+import com.google.repackaged.protobuf.TextFormat;
 import com.intellij.execution.ExecutionException;
 import com.intellij.ide.plugins.IdeaPluginDescriptor;
 import com.intellij.ide.plugins.PluginManagerCore;
 import com.intellij.openapi.project.Project;
 import com.intellij.openapi.util.BuildNumber;
+import com.intellij.openapi.util.Key;
+import java.io.BufferedInputStream;
 import java.io.File;
+import java.io.FileInputStream;
 import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
 import java.nio.file.Files;
 import java.nio.file.StandardCopyOption;
+import java.util.ArrayList;
 import java.util.Collection;
 import java.util.List;
 import java.util.Map;
-import org.jetbrains.annotations.Nullable;
+import javax.annotation.Nullable;
 
 /** Handles finding files to deploy and copying these into the sandbox. */
 class BlazeIntellijPluginDeployer {
+
+  static final Key<BlazeIntellijPluginDeployer> USER_DATA_KEY =
+      Key.create(BlazeIntellijPluginDeployer.class.getName());
+
   private final String sandboxHome;
   private final String buildNumber;
   private final TargetMap targetMap;
-  private final ArtifactLocationDecoder artifactLocationDecoder;
-  private Map<File, File> filesToDeploy = Maps.newHashMap();
+  private final List<Label> targetsToDeploy = new ArrayList<>();
+  private final List<File> deployInfoFiles = new ArrayList<>();
+  private final Map<File, File> filesToDeploy = Maps.newHashMap();
+  private File executionRoot;
 
   BlazeIntellijPluginDeployer(Project project, String sandboxHome, String buildNumber)
       throws ExecutionException {
@@ -65,17 +78,36 @@
     this.sandboxHome = sandboxHome;
     this.buildNumber = buildNumber;
     this.targetMap = blazeProjectData.targetMap;
-    this.artifactLocationDecoder = blazeProjectData.artifactLocationDecoder;
   }
 
   /** Adds an intellij plugin target to deploy */
   void addTarget(Label label) throws ExecutionException {
-    ImmutableList<IntellijPluginDeployInfo> deployInfos = findDeployInfo(label);
-    ImmutableMap<File, File> filesToDeploy = getFilesToDeploy(deployInfos);
-    this.filesToDeploy.putAll(filesToDeploy);
+    targetsToDeploy.add(label);
+  }
+
+  void reportBuildComplete(File executionRoot, BuildResultHelper buildResultHelper) {
+    this.executionRoot = executionRoot;
+    for (File file : buildResultHelper.getBuildArtifacts()) {
+      if (file.getName().endsWith(".intellij-plugin-debug-target-deploy-info")) {
+        deployInfoFiles.add(file);
+      }
+    }
   }
 
   List<String> deploy() throws ExecutionException {
+    List<IntellijPluginDeployInfo> deployInfoList = Lists.newArrayList();
+    if (!deployInfoFiles.isEmpty()) {
+      for (File deployInfoFile : deployInfoFiles) {
+        deployInfoList.addAll(readDeployInfoFromFile(deployInfoFile));
+      }
+    } else {
+      for (Label label : targetsToDeploy) {
+        deployInfoList.addAll(findDeployInfoFromBareIntelliJPluginTargets(label));
+      }
+    }
+    ImmutableMap<File, File> filesToDeploy = getFilesToDeploy(executionRoot, deployInfoList);
+    this.filesToDeploy.putAll(filesToDeploy);
+
     for (File file : filesToDeploy.keySet()) {
       if (!file.exists()) {
         throw new ExecutionException(
@@ -97,37 +129,33 @@
     }
   }
 
-  private ImmutableList<IntellijPluginDeployInfo> findDeployInfo(Label label)
+  private static ImmutableList<IntellijPluginDeployInfo> readDeployInfoFromFile(File deployInfoFile)
       throws ExecutionException {
+    ImmutableList.Builder<IntellijPluginDeployInfo> result = ImmutableList.builder();
+    try (InputStream inputStream = new BufferedInputStream(new FileInputStream(deployInfoFile))) {
+      IntellijPluginDeployInfo.Builder builder = IntellijPluginDeployInfo.newBuilder();
+      TextFormat.Parser parser = TextFormat.Parser.newBuilder().setAllowUnknownFields(true).build();
+      parser.merge(new InputStreamReader(inputStream, UTF_8), builder);
+      IntellijPluginDeployInfo deployInfo = builder.build();
+      result.add(deployInfo);
+    } catch (IOException e) {
+      throw new ExecutionException(e);
+    }
+    return result.build();
+  }
+
+  private ImmutableList<IntellijPluginDeployInfo> findDeployInfoFromBareIntelliJPluginTargets(
+      Label label) throws ExecutionException {
     TargetIdeInfo target = targetMap.get(TargetKey.forPlainTarget(label));
     if (target == null) {
       throw new ExecutionException("Target '" + label + "' not imported during sync");
     }
-    if (IntellijPluginRule.isIntellijPluginDebugTarget(target)) {
-      assert target.intellijPluginDeployInfo != null;
-      return ImmutableList.of(target.intellijPluginDeployInfo);
-    } else if (IntellijPluginRule.isSinglePluginTarget(target)) {
+    if (IntellijPluginRule.isSinglePluginTarget(target)) {
       return ImmutableList.of(deployInfoForIntellijPlugin(target));
-    } else if (IntellijPluginRule.isPluginBundle(target)) {
-      return deployInfoForLegacyBundle(target);
     }
     throw new ExecutionException("Target is not a supported intellij plugin type.");
   }
 
-  private ImmutableList<IntellijPluginDeployInfo> deployInfoForLegacyBundle(TargetIdeInfo target)
-      throws ExecutionException {
-    ImmutableList.Builder<IntellijPluginDeployInfo> deployInfoBuilder = ImmutableList.builder();
-    for (Dependency dep : target.dependencies) {
-      if (dep.dependencyType == DependencyType.COMPILE_TIME && dep.targetKey.isPlainTarget()) {
-        TargetIdeInfo depTarget = targetMap.get(dep.targetKey);
-        if (depTarget != null && IntellijPluginRule.isSinglePluginTarget(depTarget)) {
-          deployInfoBuilder.add(deployInfoForIntellijPlugin(depTarget));
-        }
-      }
-    }
-    return deployInfoBuilder.build();
-  }
-
   private static IntellijPluginDeployInfo deployInfoForIntellijPlugin(TargetIdeInfo target)
       throws ExecutionException {
     JavaIdeInfo javaIdeInfo = target.javaIdeInfo;
@@ -142,19 +170,21 @@
     if (artifact == null || artifact.classJar == null) {
       throw new ExecutionException("No output plugin jar found for '" + target + "'");
     }
-    IntellijPluginDeployFile deployFile =
-        new IntellijPluginDeployFile(
-            artifact.classJar, new File(artifact.classJar.relativePath).getName());
-    return new IntellijPluginDeployInfo(ImmutableList.of(deployFile));
+    return IntellijPluginDeployInfo.newBuilder()
+        .addDeployFiles(
+            IntellijPluginDeployFile.newBuilder()
+                .setExecutionPath(artifact.classJar.getExecutionRootRelativePath())
+                .setDeployLocation(new File(artifact.classJar.relativePath).getName()))
+        .build();
   }
 
   private ImmutableMap<File, File> getFilesToDeploy(
-      Collection<IntellijPluginDeployInfo> deployInfos) {
+      File executionRoot, Collection<IntellijPluginDeployInfo> deployInfos) {
     ImmutableMap.Builder<File, File> result = ImmutableMap.builder();
     for (IntellijPluginDeployInfo deployInfo : deployInfos) {
-      for (IntellijPluginDeployFile deployFile : deployInfo.deployFiles) {
-        File src = artifactLocationDecoder.decode(deployFile.src);
-        File dest = new File(sandboxPluginDirectory(sandboxHome), deployFile.deployLocation);
+      for (IntellijPluginDeployFile deployFile : deployInfo.getDeployFilesList()) {
+        File src = new File(executionRoot, deployFile.getExecutionPath());
+        File dest = new File(sandboxPluginDirectory(sandboxHome), deployFile.getDeployLocation());
         result.put(src, dest);
       }
     }
diff --git a/plugin_dev/src/com/google/idea/blaze/plugin/run/BuildPluginBeforeRunTaskProvider.java b/plugin_dev/src/com/google/idea/blaze/plugin/run/BuildPluginBeforeRunTaskProvider.java
index 2a9d071..3c778fe 100644
--- a/plugin_dev/src/com/google/idea/blaze/plugin/run/BuildPluginBeforeRunTaskProvider.java
+++ b/plugin_dev/src/com/google/idea/blaze/plugin/run/BuildPluginBeforeRunTaskProvider.java
@@ -19,8 +19,12 @@
 import com.google.common.util.concurrent.ListenableFuture;
 import com.google.idea.blaze.base.async.executor.BlazeExecutor;
 import com.google.idea.blaze.base.async.process.ExternalTask;
-import com.google.idea.blaze.base.async.process.LineProcessingOutputStream;
 import com.google.idea.blaze.base.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.buildresult.BuildResultHelper;
+import com.google.idea.blaze.base.command.info.BlazeInfo;
+import com.google.idea.blaze.base.command.info.BlazeInfoRunner;
 import com.google.idea.blaze.base.experiments.ExperimentScope;
 import com.google.idea.blaze.base.filecache.FileCaches;
 import com.google.idea.blaze.base.issueparser.IssueOutputLineProcessor;
@@ -45,6 +49,7 @@
 import com.intellij.openapi.project.Project;
 import com.intellij.openapi.util.Key;
 import icons.BlazeIcons;
+import java.io.File;
 import java.util.concurrent.CancellationException;
 import java.util.concurrent.ExecutionException;
 import javax.annotation.Nullable;
@@ -159,20 +164,64 @@
               new ScopedTask(context) {
                 @Override
                 protected void execute(BlazeContext context) {
+                  BlazeIntellijPluginDeployer deployer =
+                      env.getUserData(BlazeIntellijPluginDeployer.USER_DATA_KEY);
+                  if (deployer == null) {
+                    IssueOutput.error("Could not find BlazeIntellijPluginDeployer in env.")
+                        .submit(context);
+                    return;
+                  }
+
+                  String binaryPath = Blaze.getBuildSystemProvider(project).getBinaryPath();
+                  WorkspaceRoot workspaceRoot = WorkspaceRoot.fromProject(project);
                   BlazeIntellijPluginConfiguration config =
                       (BlazeIntellijPluginConfiguration) configuration;
-                  BlazeCommand command = config.buildBlazeCommand(project, projectViewSet);
+
+                  ListenableFuture<String> executionRootFuture =
+                      BlazeInfoRunner.getInstance()
+                          .runBlazeInfo(
+                              context,
+                              binaryPath,
+                              workspaceRoot,
+                              config.getBlazeFlagsState().getExpandedFlags(),
+                              BlazeInfo.EXECUTION_ROOT_KEY);
+
+                  String executionRoot;
+                  try {
+                    executionRoot = executionRootFuture.get();
+                  } catch (InterruptedException e) {
+                    Thread.currentThread().interrupt();
+                    context.setCancelled();
+                    return;
+                  } catch (ExecutionException e) {
+                    IssueOutput.error(e.getMessage()).submit(context);
+                    context.setHasError();
+                    return;
+                  }
+                  if (executionRoot == null) {
+                    IssueOutput.error("Could not determine execution root").submit(context);
+                    return;
+                  }
+
+                  BuildResultHelper buildResultHelper = BuildResultHelper.forFiles(f -> true);
+                  BlazeCommand command =
+                      BlazeCommand.builder(binaryPath, BlazeCommandName.BUILD)
+                          .addTargets(config.getTarget())
+                          .addBlazeFlags(BlazeFlags.buildFlags(project, projectViewSet))
+                          .addBlazeFlags(config.getBlazeFlagsState().getExpandedFlags())
+                          .addExeFlags(config.getExeFlagsState().getExpandedFlags())
+                          .addBlazeFlags(buildResultHelper.getBuildFlags())
+                          .build();
                   if (command == null || context.hasErrors() || context.isCancelled()) {
                     return;
                   }
                   SaveUtil.saveAllFiles();
-                  WorkspaceRoot workspaceRoot = WorkspaceRoot.fromProject(project);
                   int retVal =
                       ExternalTask.builder(workspaceRoot)
                           .addBlazeCommand(command)
                           .context(context)
                           .stderr(
-                              LineProcessingOutputStream.of(
+                              buildResultHelper.stderr(
                                   new IssueOutputLineProcessor(project, context, workspaceRoot)))
                           .build()
                           .run();
@@ -180,6 +229,7 @@
                     context.setHasError();
                   }
                   FileCaches.refresh(project);
+                  deployer.reportBuildComplete(new File(executionRoot), buildResultHelper);
                 }
               };
 
diff --git a/proto_deps/BUILD b/proto/BUILD
similarity index 100%
rename from proto_deps/BUILD
rename to proto/BUILD
diff --git a/proto/proto_deps.jar b/proto/proto_deps.jar
new file mode 100755
index 0000000..2637779
--- /dev/null
+++ b/proto/proto_deps.jar
Binary files differ
diff --git a/proto_deps/proto_deps.jar b/proto_deps/proto_deps.jar
deleted file mode 100755
index ac441be..0000000
--- a/proto_deps/proto_deps.jar
+++ /dev/null
Binary files differ
diff --git a/python/BUILD b/python/BUILD
new file mode 100644
index 0000000..51e2752
--- /dev/null
+++ b/python/BUILD
@@ -0,0 +1,99 @@
+licenses(["notice"])  # Apache 2.0
+
+load(
+    "//build_defs:build_defs.bzl",
+    "intellij_plugin",
+    "merged_plugin_xml",
+    "optional_plugin_xml",
+    "stamped_plugin_xml",
+)
+load(
+    "//testing:test_defs.bzl",
+    "intellij_integration_test_suite",
+    "intellij_unit_test_suite",
+)
+
+java_library(
+    name = "python",
+    srcs = glob(["src/**/*.java"]),
+    visibility = ["//visibility:public"],
+    deps = [
+        "//base",
+        "//common/experiments",
+        "//intellij_platform_sdk:plugin_api",
+        "//sdkcompat",
+        "//third_party/python",
+        "@jsr305_annotations//jar",
+    ],
+)
+
+filegroup(
+    name = "plugin_xml",
+    srcs = ["src/META-INF/blaze-python.xml"],
+    visibility = ["//visibility:public"],
+)
+
+optional_plugin_xml(
+    name = "optional_xml",
+    module = "com.intellij.modules.python",
+    plugin_xml = "src/META-INF/python-contents.xml",
+    visibility = ["//visibility:public"],
+)
+
+merged_plugin_xml(
+    name = "merged_plugin_xml",
+    srcs = [
+        "//base:plugin_xml",
+        "//python:plugin_xml",
+    ],
+)
+
+stamped_plugin_xml(
+    name = "python_plugin_xml",
+    plugin_id = "com.google.idea.blaze.python",
+    plugin_name = "com.google.idea.blaze.python",
+    plugin_xml = "merged_plugin_xml",
+)
+
+intellij_plugin(
+    name = "python_integration_test_plugin",
+    testonly = 1,
+    optional_plugin_xmls = [":optional_xml"],
+    plugin_xml = ":python_plugin_xml",
+    deps = [
+        "//python",
+    ],
+)
+
+intellij_integration_test_suite(
+    name = "integration_tests",
+    srcs = glob(["tests/integrationtests/**/*.java"]),
+    required_plugins = "com.google.idea.blaze.python",
+    test_package_root = "com.google.idea.blaze.python",
+    runtime_deps = [
+        ":python_integration_test_plugin",
+    ],
+    deps = [
+        ":python",
+        "//base",
+        "//base:integration_test_utils",
+        "//base:unit_test_utils",
+        "//intellij_platform_sdk:plugin_api_for_tests",
+        "//third_party/python:python_for_tests",
+        "@jsr305_annotations//jar",
+        "@junit//jar",
+    ],
+)
+
+intellij_unit_test_suite(
+    name = "unit_tests",
+    srcs = glob(["tests/unittests/**/*.java"]),
+    test_package_root = "com.google.idea.blaze.python",
+    deps = [
+        ":python",
+        "//base",
+        "//intellij_platform_sdk:plugin_api_for_tests",
+        "@jsr305_annotations//jar",
+        "@junit//jar",
+    ],
+)
diff --git a/python/src/META-INF/blaze-python.xml b/python/src/META-INF/blaze-python.xml
new file mode 100644
index 0000000..8ee292f
--- /dev/null
+++ b/python/src/META-INF/blaze-python.xml
@@ -0,0 +1,20 @@
+<!--
+  ~ Copyright 2017 The Bazel Authors. All rights reserved.
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~   http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
+<idea-plugin>
+  <extensions defaultExtensionNs="com.google.idea.blaze">
+    <SyncPlugin implementation="com.google.idea.blaze.python.sync.AlwaysPresentPythonSyncPlugin"/>
+  </extensions>
+</idea-plugin>
\ No newline at end of file
diff --git a/python/src/META-INF/python-contents.xml b/python/src/META-INF/python-contents.xml
new file mode 100644
index 0000000..b2e1ab4
--- /dev/null
+++ b/python/src/META-INF/python-contents.xml
@@ -0,0 +1,47 @@
+<!--
+  ~ Copyright 2017 The Bazel Authors. All rights reserved.
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~   http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
+<idea-plugin>
+
+  <extensions defaultExtensionNs="com.google.idea.blaze">
+    <SyncPlugin implementation="com.google.idea.blaze.python.sync.BlazePythonSyncPlugin"/>
+    <PrefetchFileSource implementation="com.google.idea.blaze.python.sync.PythonPrefetchFileSource"/>
+    <BlazeCommandRunConfigurationHandlerProvider implementation="com.google.idea.blaze.python.run.BlazePyRunConfigurationHandlerProvider" order="first"/>
+    <RunConfigurationFactory implementation="com.google.idea.blaze.python.run.BlazePyDebuggableRunConfigurationFactory"/>
+    <BlazeTestEventsHandler implementation="com.google.idea.blaze.python.run.smrunner.BlazePythonTestEventsHandler"/>
+  </extensions>
+
+  <extensions defaultExtensionNs="com.intellij">
+    <useScopeEnlarger implementation="com.google.idea.blaze.python.search.BlazePyUseScopeEnlarger"/>
+    <runConfigurationProducer implementation="com.google.idea.blaze.python.run.producers.BlazePyBinaryConfigurationProducer" order="first"/>
+    <runConfigurationProducer implementation="com.google.idea.blaze.python.run.producers.BlazePyTestConfigurationProducer" order="first"/>
+    <programRunner implementation="com.google.idea.blaze.python.run.BlazePyDebugRunner"/>
+    <consoleFilterProvider implementation="com.google.idea.blaze.python.run.filter.BlazePyTracebackFilter$BlazePyTracebackFilterProvider"/>
+  </extensions>
+
+  <extensionPoints>
+    <extensionPoint qualifiedName="com.google.idea.blaze.BlazePyFilterProvider"
+      interface="com.google.idea.blaze.python.run.filter.BlazePyFilterProvider"/>
+    <extensionPoint qualifiedName="com.google.idea.blaze.BlazePyDebugFlagsProvider"
+      interface="com.google.idea.blaze.python.run.BlazePyDebugHelper"/>
+  </extensionPoints>
+
+  <project-components>
+    <component>
+      <implementation-class>com.google.idea.blaze.python.run.producers.NonBlazeProducerSuppressor</implementation-class>
+    </component>
+  </project-components>
+
+</idea-plugin>
diff --git a/python/src/com/google/idea/blaze/python/PySdkUtils.java b/python/src/com/google/idea/blaze/python/PySdkUtils.java
new file mode 100644
index 0000000..0b56d45
--- /dev/null
+++ b/python/src/com/google/idea/blaze/python/PySdkUtils.java
@@ -0,0 +1,41 @@
+/*
+ * Copyright 2017 The Bazel Authors. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.idea.blaze.python;
+
+import com.google.idea.blaze.base.sync.data.BlazeDataStorage;
+import com.intellij.openapi.module.ModuleManager;
+import com.intellij.openapi.project.Project;
+import com.intellij.openapi.projectRoots.Sdk;
+import com.intellij.openapi.roots.ProjectRootManager;
+import com.jetbrains.python.sdk.PythonSdkType;
+import javax.annotation.Nullable;
+
+/** Helper methods related to the python SDK. */
+public class PySdkUtils {
+
+  /** Find a python SDK associated with a blaze project, or its workspace module. */
+  @Nullable
+  public static Sdk getPythonSdk(Project project) {
+    Sdk projectSdk = ProjectRootManager.getInstance(project).getProjectSdk();
+    if (projectSdk != null && projectSdk.getSdkType() instanceof PythonSdkType) {
+      return projectSdk;
+    }
+    // look for a SDK associated with a python facet instead.
+    return PythonSdkType.findPythonSdk(
+        ModuleManager.getInstance(project)
+            .findModuleByName(BlazeDataStorage.WORKSPACE_MODULE_NAME));
+  }
+}
diff --git a/python/src/com/google/idea/blaze/python/PythonPluginUtils.java b/python/src/com/google/idea/blaze/python/PythonPluginUtils.java
new file mode 100644
index 0000000..62fd0ab
--- /dev/null
+++ b/python/src/com/google/idea/blaze/python/PythonPluginUtils.java
@@ -0,0 +1,40 @@
+/*
+ * Copyright 2017 The Bazel Authors. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.idea.blaze.python;
+
+import com.google.common.collect.ImmutableMap;
+import com.intellij.util.PlatformUtils;
+
+/** Utilities class related to the JetBrains python plugin. */
+public final class PythonPluginUtils {
+
+  private PythonPluginUtils() {}
+
+  private static final ImmutableMap<String, String> PRODUCT_TO_PLUGIN_ID =
+      ImmutableMap.of(
+          PlatformUtils.IDEA_PREFIX, "Pythonid",
+          PlatformUtils.IDEA_CE_PREFIX, "PythonCore",
+          PlatformUtils.CLION_PREFIX, "com.intellij.clion-python");
+
+  public static String getPythonPluginId() {
+    String pluginId = PRODUCT_TO_PLUGIN_ID.get(PlatformUtils.getPlatformPrefix());
+    if (pluginId == null) {
+      throw new RuntimeException(
+          "No python plugin ID for unhandled platform: " + PlatformUtils.getPlatformPrefix());
+    }
+    return pluginId;
+  }
+}
diff --git a/python/src/com/google/idea/blaze/python/run/BlazePyDebugHelper.java b/python/src/com/google/idea/blaze/python/run/BlazePyDebugHelper.java
new file mode 100644
index 0000000..9418e2c
--- /dev/null
+++ b/python/src/com/google/idea/blaze/python/run/BlazePyDebugHelper.java
@@ -0,0 +1,57 @@
+/*
+ * Copyright 2017 The Bazel Authors. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.idea.blaze.python.run;
+
+import com.google.common.collect.ImmutableList;
+import com.google.idea.blaze.base.model.primitives.TargetExpression;
+import com.intellij.openapi.extensions.ExtensionPointName;
+import com.intellij.openapi.project.Project;
+import javax.annotation.Nullable;
+
+/** Extension point for adding blaze flags when debugging python targets. */
+public interface BlazePyDebugHelper {
+
+  ExtensionPointName<BlazePyDebugHelper> EP_NAME =
+      ExtensionPointName.create("com.google.idea.blaze.BlazePyDebugFlagsProvider");
+
+  static ImmutableList<String> getAllBlazeDebugFlags() {
+    ImmutableList.Builder<String> builder = ImmutableList.builder();
+    for (BlazePyDebugHelper provider : EP_NAME.getExtensions()) {
+      builder.addAll(provider.getBlazeDebugFlags());
+    }
+    return builder.build();
+  }
+
+  @Nullable
+  static String validateDebugTarget(Project project, @Nullable TargetExpression target) {
+    for (BlazePyDebugHelper provider : EP_NAME.getExtensions()) {
+      String error = provider.validatePyDebugTarget(project, target);
+      if (error != null) {
+        return error;
+      }
+    }
+    return null;
+  }
+
+  ImmutableList<String> getBlazeDebugFlags();
+
+  /**
+   * Attempts to check whether the given target can be debugged by the Blaze plugin. If there's a
+   * known problem, returns an error message with the details.
+   */
+  @Nullable
+  String validatePyDebugTarget(Project project, @Nullable TargetExpression target);
+}
diff --git a/python/src/com/google/idea/blaze/python/run/BlazePyDebugRunner.java b/python/src/com/google/idea/blaze/python/run/BlazePyDebugRunner.java
new file mode 100644
index 0000000..8b24142
--- /dev/null
+++ b/python/src/com/google/idea/blaze/python/run/BlazePyDebugRunner.java
@@ -0,0 +1,86 @@
+/*
+ * Copyright 2017 The Bazel Authors. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.idea.blaze.python.run;
+
+import com.google.idea.blaze.base.command.BlazeCommandName;
+import com.google.idea.blaze.base.run.BlazeCommandRunConfiguration;
+import com.google.idea.blaze.base.run.state.BlazeCommandRunConfigurationCommonState;
+import com.google.idea.blaze.python.run.BlazePyRunConfigurationRunner.BlazePyDummyRunProfileState;
+import com.intellij.execution.ExecutionException;
+import com.intellij.execution.ExecutionResult;
+import com.intellij.execution.configurations.RunProfile;
+import com.intellij.execution.configurations.RunProfileState;
+import com.intellij.execution.executors.DefaultDebugExecutor;
+import com.intellij.execution.runners.ExecutionEnvironment;
+import com.intellij.execution.ui.RunContentDescriptor;
+import com.intellij.xdebugger.XDebugSession;
+import com.jetbrains.python.debugger.PyDebugProcess;
+import com.jetbrains.python.debugger.PyDebugRunner;
+import com.jetbrains.python.run.PythonCommandLineState;
+import java.net.ServerSocket;
+
+/** Blaze plugin specific {@link PyDebugRunner}. */
+public class BlazePyDebugRunner extends PyDebugRunner {
+
+  @Override
+  public String getRunnerId() {
+    return "BlazePyDebugRunner";
+  }
+
+  @Override
+  public boolean canRun(String executorId, RunProfile profile) {
+    if (!DefaultDebugExecutor.EXECUTOR_ID.equals(executorId)
+        || !(profile instanceof BlazeCommandRunConfiguration)) {
+      return false;
+    }
+    BlazeCommandRunConfiguration config = (BlazeCommandRunConfiguration) profile;
+    BlazeCommandRunConfigurationCommonState handlerState =
+        config.getHandlerStateIfType(BlazeCommandRunConfigurationCommonState.class);
+    BlazeCommandName command =
+        handlerState != null ? handlerState.getCommandState().getCommand() : null;
+    return PyDebugUtils.canUsePyDebugger(config.getKindForTarget())
+        && (BlazeCommandName.TEST.equals(command) || BlazeCommandName.RUN.equals(command));
+  }
+
+  @Override
+  protected PyDebugProcess createDebugProcess(
+      XDebugSession xDebugSession,
+      ServerSocket serverSocket,
+      ExecutionResult executionResult,
+      PythonCommandLineState pythonCommandLineState) {
+    PyDebugProcess process =
+        super.createDebugProcess(
+            xDebugSession, serverSocket, executionResult, pythonCommandLineState);
+    process.setPositionConverter(new BlazePyPositionConverter());
+    return process;
+  }
+
+  @Override
+  protected RunContentDescriptor doExecute(RunProfileState state, ExecutionEnvironment environment)
+      throws ExecutionException {
+    if (!(state instanceof BlazePyDummyRunProfileState)) {
+      return null;
+    }
+    try {
+      state = ((BlazePyDummyRunProfileState) state).toNativeState(environment);
+      return super.doExecute(state, environment);
+    } catch (ExecutionException e) {
+      throw e;
+    } catch (Exception e) {
+      throw new ExecutionException(e);
+    }
+  }
+}
diff --git a/python/src/com/google/idea/blaze/python/run/BlazePyDebuggableRunConfigurationFactory.java b/python/src/com/google/idea/blaze/python/run/BlazePyDebuggableRunConfigurationFactory.java
new file mode 100644
index 0000000..7536401
--- /dev/null
+++ b/python/src/com/google/idea/blaze/python/run/BlazePyDebuggableRunConfigurationFactory.java
@@ -0,0 +1,62 @@
+/*
+ * Copyright 2017 The Bazel Authors. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.idea.blaze.python.run;
+
+import com.google.idea.blaze.base.command.BlazeCommandName;
+import com.google.idea.blaze.base.ideinfo.TargetIdeInfo;
+import com.google.idea.blaze.base.ideinfo.TargetKey;
+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.run.BlazeCommandRunConfiguration;
+import com.google.idea.blaze.base.run.BlazeCommandRunConfigurationType;
+import com.google.idea.blaze.base.run.BlazeRunConfigurationFactory;
+import com.google.idea.blaze.base.run.state.BlazeCommandRunConfigurationCommonState;
+import com.intellij.execution.configurations.ConfigurationFactory;
+import com.intellij.execution.configurations.RunConfiguration;
+import com.intellij.openapi.project.Project;
+
+/** Creates run configurations for debuggable python targets. */
+public class BlazePyDebuggableRunConfigurationFactory extends BlazeRunConfigurationFactory {
+  @Override
+  public boolean handlesTarget(Project project, BlazeProjectData blazeProjectData, Label label) {
+    TargetIdeInfo target = blazeProjectData.targetMap.get(TargetKey.forPlainTarget(label));
+    return target != null && target.kind != null && PyDebugUtils.canUsePyDebugger(target.kind);
+  }
+
+  @Override
+  protected ConfigurationFactory getConfigurationFactory() {
+    return BlazeCommandRunConfigurationType.getInstance().getFactory();
+  }
+
+  @Override
+  public void setupConfiguration(RunConfiguration configuration, Label target) {
+    final BlazeCommandRunConfiguration blazeConfig = (BlazeCommandRunConfiguration) configuration;
+    blazeConfig.setTarget(target);
+
+    BlazeCommandRunConfigurationCommonState state =
+        blazeConfig.getHandlerStateIfType(BlazeCommandRunConfigurationCommonState.class);
+    Kind kind = blazeConfig.getKindForTarget();
+    if (state != null) {
+      BlazeCommandName command =
+          kind != null && Kind.isTestRule(kind.toString())
+              ? BlazeCommandName.TEST
+              : BlazeCommandName.RUN;
+      state.getCommandState().setCommand(command);
+    }
+    blazeConfig.setGeneratedName();
+  }
+}
diff --git a/python/src/com/google/idea/blaze/python/run/BlazePyPositionConverter.java b/python/src/com/google/idea/blaze/python/run/BlazePyPositionConverter.java
new file mode 100644
index 0000000..7dd68d1
--- /dev/null
+++ b/python/src/com/google/idea/blaze/python/run/BlazePyPositionConverter.java
@@ -0,0 +1,128 @@
+/*
+ * Copyright 2017 The Bazel Authors. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.idea.blaze.python.run;
+
+import com.intellij.openapi.application.AccessToken;
+import com.intellij.openapi.application.ApplicationManager;
+import com.intellij.openapi.diagnostic.Logger;
+import com.intellij.openapi.editor.Document;
+import com.intellij.openapi.fileEditor.FileDocumentManager;
+import com.intellij.openapi.util.Computable;
+import com.intellij.openapi.vfs.LocalFileSystem;
+import com.intellij.openapi.vfs.VirtualFile;
+import com.intellij.xdebugger.XDebuggerUtil;
+import com.intellij.xdebugger.XSourcePosition;
+import com.jetbrains.python.debugger.PyDebugSupportUtils;
+import com.jetbrains.python.debugger.PyLocalPositionConverter;
+import com.jetbrains.python.debugger.PyPositionConverter;
+import com.jetbrains.python.debugger.PySignature;
+import com.jetbrains.python.debugger.PySourcePosition;
+import java.io.File;
+import java.io.IOException;
+import javax.annotation.Nullable;
+
+/**
+ * Converts the blaze-out symlink file paths to the actual source file paths. Otherwise a copy of
+ * {@link PyLocalPositionConverter}
+ */
+public class BlazePyPositionConverter implements PyPositionConverter {
+
+  private static final Logger logger = Logger.getInstance(BlazePyPositionConverter.class);
+
+  @Override
+  public PySourcePosition create(String filePath, int line) {
+    return new PySourcePosition(convertFilePath(filePath), line) {};
+  }
+
+  @Override
+  public PySourcePosition convertToPython(XSourcePosition position) {
+    return new PySourcePosition(
+        convertFilePath(position.getFile().getPath()),
+        convertLocalLineToRemote(position.getFile(), position.getLine())) {};
+  }
+
+  @Nullable
+  @Override
+  public XSourcePosition convertFromPython(PySourcePosition position) {
+    return createXSourcePosition(getVirtualFile(position.getFile()), position.getLine());
+  }
+
+  @Override
+  public PySignature convertSignature(PySignature signature) {
+    return signature;
+  }
+
+  private static String convertFilePath(String filePath) {
+    File file = new File(filePath);
+    try {
+      return file.getCanonicalPath();
+    } catch (IOException e) {
+      logger.warn(e);
+      return filePath;
+    }
+  }
+
+  private static VirtualFile getVirtualFile(String filePath) {
+    return LocalFileSystem.getInstance().findFileByPath(filePath);
+  }
+
+  @Nullable
+  private static XSourcePosition createXSourcePosition(@Nullable VirtualFile vFile, int line) {
+    if (vFile != null) {
+      return XDebuggerUtil.getInstance()
+          .createPosition(vFile, convertRemoteLineToLocal(vFile, line));
+    } else {
+      return null;
+    }
+  }
+
+  /** Convert from 1- to 0-indexed line numbering, and account for continuation lines */
+  private static int convertLocalLineToRemote(VirtualFile file, int line) {
+    AccessToken lock = ApplicationManager.getApplication().acquireReadActionLock();
+    try {
+      final Document document = FileDocumentManager.getInstance().getDocument(file);
+      if (document != null) {
+        while (PyDebugSupportUtils.isContinuationLine(document, line)) {
+          line++;
+        }
+      }
+      return line + 1;
+    } finally {
+      lock.finish();
+    }
+  }
+
+  /** Convert from 0- to 1-indexed line numbering, and account for continuation lines */
+  private static int convertRemoteLineToLocal(final VirtualFile vFile, int line) {
+    final Document document =
+        ApplicationManager.getApplication()
+            .runReadAction(
+                new Computable<Document>() {
+                  @Override
+                  public Document compute() {
+                    return FileDocumentManager.getInstance().getDocument(vFile);
+                  }
+                });
+
+    line--;
+    if (document != null) {
+      while (PyDebugSupportUtils.isContinuationLine(document, line - 1)) {
+        line--;
+      }
+    }
+    return line;
+  }
+}
diff --git a/python/src/com/google/idea/blaze/python/run/BlazePyRunConfigurationHandler.java b/python/src/com/google/idea/blaze/python/run/BlazePyRunConfigurationHandler.java
new file mode 100644
index 0000000..e8a2d3f
--- /dev/null
+++ b/python/src/com/google/idea/blaze/python/run/BlazePyRunConfigurationHandler.java
@@ -0,0 +1,87 @@
+/*
+ * Copyright 2017 The Bazel Authors. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.idea.blaze.python.run;
+
+import com.google.idea.blaze.base.command.BlazeCommandName;
+import com.google.idea.blaze.base.run.BlazeCommandRunConfiguration;
+import com.google.idea.blaze.base.run.BlazeConfigurationNameBuilder;
+import com.google.idea.blaze.base.run.confighandler.BlazeCommandRunConfigurationHandler;
+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.RuntimeConfigurationException;
+import com.intellij.execution.runners.ExecutionEnvironment;
+import javax.annotation.Nullable;
+import javax.swing.Icon;
+
+/** Python-specific handler for {@link BlazeCommandRunConfiguration}s. */
+public final class BlazePyRunConfigurationHandler implements BlazeCommandRunConfigurationHandler {
+
+  private final String buildSystemName;
+  private final BlazeCommandRunConfigurationCommonState state;
+
+  public BlazePyRunConfigurationHandler(BlazeCommandRunConfiguration configuration) {
+    BuildSystem buildSystem = Blaze.getBuildSystem(configuration.getProject());
+    this.buildSystemName = buildSystem.getName();
+    this.state = new BlazeCommandRunConfigurationCommonState(buildSystem);
+  }
+
+  @Override
+  public BlazeCommandRunConfigurationCommonState getState() {
+    return state;
+  }
+
+  @Override
+  public BlazeCommandRunConfigurationRunner createRunner(
+      Executor executor, ExecutionEnvironment environment) {
+    return new BlazePyRunConfigurationRunner();
+  }
+
+  @Override
+  public void checkConfiguration() throws RuntimeConfigurationException {
+    state.validate(buildSystemName);
+  }
+
+  @Override
+  @Nullable
+  public String suggestedName(BlazeCommandRunConfiguration configuration) {
+    if (configuration.getTarget() == null) {
+      return null;
+    }
+    return new BlazeConfigurationNameBuilder(configuration).build();
+  }
+
+  @Override
+  @Nullable
+  public String getCommandName() {
+    BlazeCommandName command = state.getCommandState().getCommand();
+    return command != null ? command.toString() : null;
+  }
+
+  @Override
+  public String getHandlerName() {
+    return "Python Handler";
+  }
+
+  @Override
+  @Nullable
+  public Icon getExecutorIcon(RunConfiguration configuration, Executor executor) {
+    return null;
+  }
+}
diff --git a/python/src/com/google/idea/blaze/python/run/BlazePyRunConfigurationHandlerProvider.java b/python/src/com/google/idea/blaze/python/run/BlazePyRunConfigurationHandlerProvider.java
new file mode 100644
index 0000000..0975c01
--- /dev/null
+++ b/python/src/com/google/idea/blaze/python/run/BlazePyRunConfigurationHandlerProvider.java
@@ -0,0 +1,41 @@
+/*
+ * Copyright 2017 The Bazel Authors. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.idea.blaze.python.run;
+
+import com.google.idea.blaze.base.model.primitives.Kind;
+import com.google.idea.blaze.base.run.BlazeCommandRunConfiguration;
+import com.google.idea.blaze.base.run.confighandler.BlazeCommandRunConfigurationHandler;
+import com.google.idea.blaze.base.run.confighandler.BlazeCommandRunConfigurationHandlerProvider;
+
+/** Python-specific handler provider for {@link BlazeCommandRunConfiguration}s. */
+public class BlazePyRunConfigurationHandlerProvider
+    implements BlazeCommandRunConfigurationHandlerProvider {
+
+  @Override
+  public boolean canHandleKind(Kind kind) {
+    return PyDebugUtils.canUsePyDebugger(kind);
+  }
+
+  @Override
+  public BlazeCommandRunConfigurationHandler createHandler(BlazeCommandRunConfiguration config) {
+    return new BlazePyRunConfigurationHandler(config);
+  }
+
+  @Override
+  public String getId() {
+    return "BlazePyRunConfigurationHandlerProvider";
+  }
+}
diff --git a/python/src/com/google/idea/blaze/python/run/BlazePyRunConfigurationRunner.java b/python/src/com/google/idea/blaze/python/run/BlazePyRunConfigurationRunner.java
new file mode 100644
index 0000000..08f2a22
--- /dev/null
+++ b/python/src/com/google/idea/blaze/python/run/BlazePyRunConfigurationRunner.java
@@ -0,0 +1,389 @@
+/*
+ * Copyright 2017 The Bazel Authors. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.idea.blaze.python.run;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.base.Strings;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Lists;
+import com.google.common.util.concurrent.ListenableFuture;
+import com.google.idea.blaze.base.async.executor.BlazeExecutor;
+import com.google.idea.blaze.base.async.process.ExternalTask;
+import com.google.idea.blaze.base.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.buildresult.BuildResultHelper;
+import com.google.idea.blaze.base.io.FileAttributeProvider;
+import com.google.idea.blaze.base.issueparser.IssueOutputLineProcessor;
+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.WorkspaceRoot;
+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.WithBrowserHyperlinkExecutionException;
+import com.google.idea.blaze.base.run.confighandler.BlazeCommandGenericRunConfigurationRunner.BlazeCommandRunProfileState;
+import com.google.idea.blaze.base.run.confighandler.BlazeCommandRunConfigurationRunner;
+import com.google.idea.blaze.base.run.filter.BlazeTargetFilter;
+import com.google.idea.blaze.base.run.state.BlazeCommandRunConfigurationCommonState;
+import com.google.idea.blaze.base.scope.BlazeContext;
+import com.google.idea.blaze.base.scope.ScopedTask;
+import com.google.idea.blaze.base.scope.output.StatusOutput;
+import com.google.idea.blaze.base.scope.scopes.BlazeConsoleScope;
+import com.google.idea.blaze.base.scope.scopes.IssuesScope;
+import com.google.idea.blaze.base.settings.Blaze;
+import com.google.idea.blaze.base.settings.BlazeUserSettings;
+import com.google.idea.blaze.base.sync.data.BlazeDataStorage;
+import com.google.idea.blaze.base.sync.data.BlazeProjectDataManager;
+import com.google.idea.blaze.base.util.SaveUtil;
+import com.google.idea.blaze.python.run.filter.BlazePyFilterProvider;
+import com.google.idea.common.experiments.BoolExperiment;
+import com.intellij.execution.ExecutionException;
+import com.intellij.execution.ExecutionResult;
+import com.intellij.execution.Executor;
+import com.intellij.execution.configurations.GeneralCommandLine;
+import com.intellij.execution.configurations.RunProfile;
+import com.intellij.execution.configurations.RunProfileState;
+import com.intellij.execution.configurations.WrappingRunConfiguration;
+import com.intellij.execution.executors.DefaultDebugExecutor;
+import com.intellij.execution.filters.Filter;
+import com.intellij.execution.filters.TextConsoleBuilder;
+import com.intellij.execution.process.KillableProcessHandler;
+import com.intellij.execution.process.ProcessHandler;
+import com.intellij.execution.runners.ExecutionEnvironment;
+import com.intellij.execution.runners.ExecutionUtil;
+import com.intellij.execution.runners.ProgramRunner;
+import com.intellij.execution.ui.ConsoleView;
+import com.intellij.openapi.diagnostic.Logger;
+import com.intellij.openapi.module.Module;
+import com.intellij.openapi.project.Project;
+import com.intellij.openapi.projectRoots.Sdk;
+import com.intellij.openapi.util.Key;
+import com.intellij.openapi.util.io.FileUtil;
+import com.intellij.openapi.util.text.StringUtil;
+import com.intellij.openapi.vfs.LocalFileSystem;
+import com.intellij.util.PathUtil;
+import com.intellij.util.execution.ParametersListUtil;
+import com.jetbrains.python.console.PyDebugConsoleBuilder;
+import com.jetbrains.python.console.PythonDebugLanguageConsoleView;
+import com.jetbrains.python.run.PythonConfigurationType;
+import com.jetbrains.python.run.PythonRunConfiguration;
+import com.jetbrains.python.run.PythonScriptCommandLineState;
+import java.io.File;
+import java.util.List;
+import java.util.concurrent.atomic.AtomicReference;
+import java.util.function.Predicate;
+import java.util.stream.Collectors;
+import javax.annotation.Nullable;
+
+/** Python-specific run configuration runner. */
+public class BlazePyRunConfigurationRunner implements BlazeCommandRunConfigurationRunner {
+
+  /** Used to store a runner to an {@link ExecutionEnvironment}. */
+  private static final Key<AtomicReference<File>> EXECUTABLE_KEY =
+      Key.create("blaze.debug.py.executable");
+
+  private static final Logger logger = Logger.getInstance(BlazePyRunConfigurationRunner.class);
+
+  // Filter executables instead of files in the bin directory
+  // This bin directory isn't the right one, because we don't know the blaze binary
+  // or the config flags used to execute the build command
+  // Introduced March 2017
+  private static final BoolExperiment filterExecutableFiles =
+      new BoolExperiment("filter.executable.files", true);
+
+  /** Converts to the native python plugin debug configuration state */
+  static class BlazePyDummyRunProfileState implements RunProfileState {
+    final BlazeCommandRunConfiguration configuration;
+
+    BlazePyDummyRunProfileState(BlazeCommandRunConfiguration configuration) {
+      this.configuration = configuration;
+    }
+
+    PythonScriptCommandLineState toNativeState(ExecutionEnvironment env) throws ExecutionException {
+      File executable = env.getCopyableUserData(EXECUTABLE_KEY).get();
+      if (executable == null || StringUtil.isEmptyOrSpaces(executable.getPath())) {
+        throw new ExecutionException("No blaze output script found");
+      }
+      PythonRunConfiguration nativeConfig =
+          (PythonRunConfiguration)
+              PythonConfigurationType.getInstance()
+                  .getFactory()
+                  .createTemplateConfiguration(env.getProject());
+      nativeConfig.setScriptName(executable.getPath());
+      nativeConfig.setAddContentRoots(false);
+      nativeConfig.setAddSourceRoots(false);
+      nativeConfig.setWorkingDirectory(
+          Strings.nullToEmpty(
+              getRunfilesPath(executable, WorkspaceRoot.fromProjectSafe(env.getProject()))));
+
+      Module workspaceModule =
+          nativeConfig.getConfigurationModule().findModule(BlazeDataStorage.WORKSPACE_MODULE_NAME);
+      if (workspaceModule != null) {
+        nativeConfig.setModule(workspaceModule);
+        nativeConfig.setUseModuleSdk(true);
+      } else {
+        throw new ExecutionException(
+            "Can't find the workspace module when debugging a python target");
+      }
+
+      BlazeCommandRunConfigurationCommonState handlerState =
+          configuration.getHandlerStateIfType(BlazeCommandRunConfigurationCommonState.class);
+      if (handlerState != null) {
+        nativeConfig.setScriptParameters(Strings.emptyToNull(getScriptParams(handlerState)));
+      }
+      return new PythonScriptCommandLineState(nativeConfig, env) {
+        @Override
+        public boolean isDebug() {
+          return true;
+        }
+
+        @Override
+        protected ConsoleView createAndAttachConsole(
+            Project project, ProcessHandler processHandler, Executor executor)
+            throws ExecutionException {
+          ConsoleView consoleView = createConsoleBuilder(project, getSdk()).getConsole();
+          consoleView.addMessageFilter(createUrlFilter(processHandler));
+          addTracebackFilter(project, consoleView, processHandler);
+
+          consoleView.attachToProcess(processHandler);
+          return consoleView;
+        }
+
+        @Override
+        protected ProcessHandler doCreateProcess(GeneralCommandLine commandLine)
+            throws ExecutionException {
+          ProcessHandler handler = super.doCreateProcess(commandLine);
+          if (handler instanceof KillableProcessHandler) {
+            // SIGINT can cause the JVM to crash, when stopped at a breakpoint (IDEA-167432).
+            ((KillableProcessHandler) handler).setShouldKillProcessSoftly(false);
+          }
+          return handler;
+        }
+      };
+    }
+
+    @Nullable
+    @Override
+    public ExecutionResult execute(Executor executor, ProgramRunner runner)
+        throws ExecutionException {
+      return null;
+    }
+
+    private static TextConsoleBuilder createConsoleBuilder(Project project, Sdk sdk) {
+      return new PyDebugConsoleBuilder(project, sdk) {
+        @Override
+        protected ConsoleView createConsole() {
+          PythonDebugLanguageConsoleView consoleView =
+              new PythonDebugLanguageConsoleView(project, sdk);
+          for (Filter filter : getFilters(project)) {
+            consoleView.addMessageFilter(filter);
+          }
+          return consoleView;
+        }
+      };
+    }
+
+    private static String getScriptParams(BlazeCommandRunConfigurationCommonState state) {
+      List<String> params = Lists.newArrayList(state.getExeFlagsState().getExpandedFlags());
+      String filterFlag = state.getTestFilterFlag();
+      if (filterFlag != null) {
+        params.add(filterFlag.substring((BlazeFlags.TEST_FILTER + "=").length()));
+      }
+      return ParametersListUtil.join(params);
+    }
+  }
+
+  private static ImmutableList<Filter> getFilters(Project project) {
+    return ImmutableList.<Filter>builder()
+        .addAll(BlazePyFilterProvider.getPyFilters(project))
+        .add(new BlazeTargetFilter(project))
+        .build();
+  }
+
+  @Override
+  public RunProfileState getRunProfileState(Executor executor, ExecutionEnvironment environment)
+      throws ExecutionException {
+    BlazeCommandRunConfiguration configuration = getConfiguration(environment);
+    if (isDebugging(environment)) {
+      environment.putCopyableUserData(EXECUTABLE_KEY, new AtomicReference<>());
+      return new BlazePyDummyRunProfileState(configuration);
+    }
+    return new BlazeCommandRunProfileState(environment, getFilters(environment.getProject()));
+  }
+
+  @Override
+  public boolean executeBeforeRunTask(ExecutionEnvironment env) {
+    if (!isDebugging(env)) {
+      return true;
+    }
+    try {
+      File executable = getExecutableToDebug(env);
+      env.getCopyableUserData(EXECUTABLE_KEY).set(executable);
+      if (executable != null) {
+        return true;
+      }
+    } catch (ExecutionException e) {
+      ExecutionUtil.handleExecutionError(
+          env.getProject(), env.getExecutor().getToolWindowId(), env.getRunProfile(), e);
+      logger.info(e);
+    }
+    return false;
+  }
+
+  private static boolean isDebugging(ExecutionEnvironment environment) {
+    Executor executor = environment.getExecutor();
+    return executor instanceof DefaultDebugExecutor;
+  }
+
+  private static BlazeCommandRunConfiguration getConfiguration(ExecutionEnvironment environment) {
+    RunProfile runProfile = environment.getRunProfile();
+    if (runProfile instanceof WrappingRunConfiguration) {
+      runProfile = ((WrappingRunConfiguration) runProfile).getPeer();
+    }
+    return (BlazeCommandRunConfiguration) runProfile;
+  }
+
+  /** Make a best-effort attempt to get the runfiles path. Returns null if it can't be found. */
+  @Nullable
+  private static String getRunfilesPath(File executable, @Nullable WorkspaceRoot root) {
+    if (root == null) {
+      return null;
+    }
+    String workspaceName = root.directory().getName();
+    File expectedPath = new File(executable.getPath() + ".runfiles", workspaceName);
+    if (FileAttributeProvider.getInstance().exists(expectedPath)) {
+      return expectedPath.getPath();
+    }
+    return null;
+  }
+
+  /**
+   * Builds blaze python target and returns the output build artifact.
+   *
+   * @throws ExecutionException if the target cannot be debugged.
+   */
+  private static File getExecutableToDebug(ExecutionEnvironment env) throws ExecutionException {
+    BlazeCommandRunConfiguration configuration = getConfiguration(env);
+    final Project project = configuration.getProject();
+    BlazeProjectData blazeProjectData =
+        BlazeProjectDataManager.getInstance(project).getBlazeProjectData();
+    if (blazeProjectData == null) {
+      throw new ExecutionException("Not synced yet, please sync project");
+    }
+
+    String validationError =
+        BlazePyDebugHelper.validateDebugTarget(env.getProject(), configuration.getTarget());
+    if (validationError != null) {
+      throw new WithBrowserHyperlinkExecutionException(validationError);
+    }
+
+    final BlazeCommandRunConfigurationCommonState handlerState =
+        (BlazeCommandRunConfigurationCommonState) configuration.getHandler().getState();
+    final WorkspaceRoot workspaceRoot = WorkspaceRoot.fromProject(project);
+    final ProjectViewSet projectViewSet =
+        ProjectViewManager.getInstance(project).getProjectViewSet();
+
+    BuildResultHelper buildResultHelper = BuildResultHelper.forFiles(file -> true);
+    boolean suppressConsole = BlazeUserSettings.getInstance().getSuppressConsoleForRunAction();
+    final ListenableFuture<Void> buildOperation =
+        BlazeExecutor.submitTask(
+            project,
+            new ScopedTask() {
+              @Override
+              protected void execute(BlazeContext context) {
+                context
+                    .push(new IssuesScope(project))
+                    .push(
+                        new BlazeConsoleScope.Builder(project)
+                            .setSuppressConsole(suppressConsole)
+                            .build());
+
+                context.output(new StatusOutput("Building debug binary"));
+
+                BlazeCommand.Builder command =
+                    BlazeCommand.builder(
+                            Blaze.getBuildSystemProvider(project).getBinaryPath(),
+                            BlazeCommandName.BUILD)
+                        .addTargets(configuration.getTarget())
+                        .addBlazeFlags(BlazeFlags.buildFlags(project, projectViewSet))
+                        .addBlazeFlags(handlerState.getBlazeFlagsState().getExpandedFlags())
+                        .addBlazeFlags(BlazePyDebugHelper.getAllBlazeDebugFlags())
+                        .addBlazeFlags(buildResultHelper.getBuildFlags());
+
+                ExternalTask.builder(workspaceRoot)
+                    .addBlazeCommand(command.build())
+                    .context(context)
+                    .stderr(
+                        buildResultHelper.stderr(
+                            new IssueOutputLineProcessor(project, context, workspaceRoot)))
+                    .build()
+                    .run();
+              }
+            });
+
+    try {
+      SaveUtil.saveAllFiles();
+      buildOperation.get();
+    } catch (InterruptedException | java.util.concurrent.ExecutionException e) {
+      throw new ExecutionException(e);
+    }
+    List<File> candidateFiles =
+        buildResultHelper
+            .getBuildArtifacts()
+            .stream()
+            .filter(fileFilter(blazeProjectData))
+            .collect(Collectors.toList());
+    if (candidateFiles.isEmpty()) {
+      throw new ExecutionException(
+          String.format("No output artifacts found when building %s", configuration.getTarget()));
+    }
+    File file = findExecutable((Label) configuration.getTarget(), candidateFiles);
+    if (file == null) {
+      throw new ExecutionException(
+          String.format(
+              "More than 1 executable was produced when building %s; don't know which one to debug",
+              configuration.getTarget()));
+    }
+    LocalFileSystem.getInstance().refreshIoFiles(ImmutableList.of(file));
+    return file;
+  }
+
+  private static Predicate<File> fileFilter(BlazeProjectData blazeProjectData) {
+    return filterExecutableFiles.getValue()
+        ? File::canExecute
+        : f -> FileUtil.isAncestor(blazeProjectData.blazeInfo.getBlazeBinDirectory(), f, true);
+  }
+
+  /**
+   * Basic heuristic for choosing between multiple output files. Currently just looks for a filename
+   * matching the target name.
+   */
+  @VisibleForTesting
+  @Nullable
+  static File findExecutable(Label target, List<File> outputs) {
+    if (outputs.size() == 1) {
+      return outputs.get(0);
+    }
+    String name = PathUtil.getFileName(target.targetName().toString());
+    for (File file : outputs) {
+      if (file.getName().equals(name)) {
+        return file;
+      }
+    }
+    return null;
+  }
+}
diff --git a/python/src/com/google/idea/blaze/python/run/PyDebugUtils.java b/python/src/com/google/idea/blaze/python/run/PyDebugUtils.java
new file mode 100644
index 0000000..ec41831
--- /dev/null
+++ b/python/src/com/google/idea/blaze/python/run/PyDebugUtils.java
@@ -0,0 +1,27 @@
+/*
+ * Copyright 2017 The Bazel Authors. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.idea.blaze.python.run;
+
+import com.google.idea.blaze.base.model.primitives.Kind;
+import javax.annotation.Nullable;
+
+/** Utilities class for debuggable python run configurations. */
+public class PyDebugUtils {
+
+  static boolean canUsePyDebugger(@Nullable Kind kind) {
+    return kind != null && kind.isOneOf(Kind.PY_BINARY, Kind.PY_APPENGINE_BINARY, Kind.PY_TEST);
+  }
+}
diff --git a/python/src/com/google/idea/blaze/python/run/PyTestUtils.java b/python/src/com/google/idea/blaze/python/run/PyTestUtils.java
new file mode 100644
index 0000000..4493706
--- /dev/null
+++ b/python/src/com/google/idea/blaze/python/run/PyTestUtils.java
@@ -0,0 +1,61 @@
+/*
+ * Copyright 2017 The Bazel Authors. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.idea.blaze.python.run;
+
+import com.google.common.collect.ImmutableSet;
+import com.jetbrains.python.psi.PyClass;
+import com.jetbrains.python.psi.PyFile;
+import com.jetbrains.python.psi.PyFunction;
+import com.jetbrains.python.psi.types.PyClassLikeType;
+import com.jetbrains.python.psi.types.TypeEvalContext;
+
+/** Utilities class for identifying python test psi elements. */
+public class PyTestUtils {
+
+  private static final ImmutableSet<String> PY_UNIT_TEST_CLASSES =
+      ImmutableSet.of("unittest.TestCase", "unittest.case.TestCase");
+
+  public static boolean isTestFile(PyFile file) {
+    for (PyClass cls : file.getTopLevelClasses()) {
+      if (isTestClass(cls)) {
+        return true;
+      }
+    }
+    for (PyFunction cls : file.getTopLevelFunctions()) {
+      if (isTestFunction(cls)) {
+        return true;
+      }
+    }
+    return false;
+  }
+
+  public static boolean isTestClass(PyClass pyClass) {
+    final TypeEvalContext contextToUse =
+        TypeEvalContext.userInitiated(pyClass.getProject(), pyClass.getContainingFile());
+    for (PyClassLikeType type : pyClass.getAncestorTypes(contextToUse)) {
+      if (type != null && PY_UNIT_TEST_CLASSES.contains(type.getClassQName())) {
+        return true;
+      }
+    }
+    String name = pyClass.getName();
+    return name != null && name.endsWith("Test");
+  }
+
+  public static boolean isTestFunction(PyFunction fn) {
+    String name = fn.getName();
+    return name != null && name.startsWith("test");
+  }
+}
diff --git a/python/src/com/google/idea/blaze/python/run/filter/BlazePyFilterProvider.java b/python/src/com/google/idea/blaze/python/run/filter/BlazePyFilterProvider.java
new file mode 100644
index 0000000..2507b06
--- /dev/null
+++ b/python/src/com/google/idea/blaze/python/run/filter/BlazePyFilterProvider.java
@@ -0,0 +1,38 @@
+/*
+ * Copyright 2017 The Bazel Authors. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.idea.blaze.python.run.filter;
+
+import com.intellij.execution.filters.Filter;
+import com.intellij.openapi.extensions.ExtensionPointName;
+import com.intellij.openapi.project.Project;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.stream.Collectors;
+
+/** Filters for python run configuration console output. */
+public interface BlazePyFilterProvider {
+
+  ExtensionPointName<BlazePyFilterProvider> EP_NAME =
+      ExtensionPointName.create("com.google.idea.blaze.BlazePyFilterProvider");
+
+  static Collection<Filter> getPyFilters(Project project) {
+    return Arrays.stream(EP_NAME.getExtensions())
+        .map(provider -> provider.getFilter(project))
+        .collect(Collectors.toList());
+  }
+
+  Filter getFilter(Project project);
+}
diff --git a/python/src/com/google/idea/blaze/python/run/filter/BlazePyTracebackFilter.java b/python/src/com/google/idea/blaze/python/run/filter/BlazePyTracebackFilter.java
new file mode 100644
index 0000000..a13892d
--- /dev/null
+++ b/python/src/com/google/idea/blaze/python/run/filter/BlazePyTracebackFilter.java
@@ -0,0 +1,84 @@
+/*
+ * Copyright 2017 The Bazel Authors. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.idea.blaze.python.run.filter;
+
+import com.google.idea.blaze.base.run.filter.FileResolver;
+import com.intellij.execution.filters.ConsoleFilterProvider;
+import com.intellij.execution.filters.Filter;
+import com.intellij.execution.filters.OpenFileHyperlinkInfo;
+import com.intellij.openapi.project.Project;
+import com.intellij.openapi.vfs.VirtualFile;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+import javax.annotation.Nullable;
+
+/** Parses traceback links in python test results */
+public class BlazePyTracebackFilter implements Filter {
+
+  /** Provider for traceback filter */
+  public static class BlazePyTracebackFilterProvider implements ConsoleFilterProvider {
+    @Override
+    public Filter[] getDefaultFilters(Project project) {
+      return new Filter[] {new BlazePyTracebackFilter(project)};
+    }
+  }
+
+  private static final Pattern TRACEBACK_FILE_LINE =
+      Pattern.compile("File \"(.*?)\", line ([0-9]+), in (.*?)");
+
+  private final Project project;
+
+  private BlazePyTracebackFilter(Project project) {
+    this.project = project;
+  }
+
+  @Nullable
+  @Override
+  public Result applyFilter(String line, int entireLength) {
+    Matcher matcher = TRACEBACK_FILE_LINE.matcher(line);
+    if (!matcher.find()) {
+      return null;
+    }
+    String filePath = matcher.group(1);
+    if (filePath == null) {
+      return null;
+    }
+    VirtualFile file = FileResolver.resolve(project, filePath);
+    if (file == null) {
+      return null;
+    }
+    int lineNumber = parseLineNumber(matcher.group(2));
+    OpenFileHyperlinkInfo hyperlink = new OpenFileHyperlinkInfo(project, file, lineNumber - 1);
+
+    int startIx = matcher.start(2) - "line ".length();
+    int endIx = matcher.end(2);
+    if (startIx < 0) {
+      startIx = matcher.start(1);
+      endIx = matcher.end(1);
+    }
+    int offset = entireLength - line.length();
+    return new Result(startIx + offset, endIx + offset, hyperlink);
+  }
+
+  /** defaults to -1 if no line number can be parsed. */
+  private static int parseLineNumber(@Nullable String string) {
+    try {
+      return string != null ? Integer.parseInt(string) : -1;
+    } catch (NumberFormatException e) {
+      return -1;
+    }
+  }
+}
diff --git a/python/src/com/google/idea/blaze/python/run/producers/BlazePyBinaryConfigurationProducer.java b/python/src/com/google/idea/blaze/python/run/producers/BlazePyBinaryConfigurationProducer.java
new file mode 100644
index 0000000..1b61af0
--- /dev/null
+++ b/python/src/com/google/idea/blaze/python/run/producers/BlazePyBinaryConfigurationProducer.java
@@ -0,0 +1,139 @@
+/*
+ * Copyright 2017 The Bazel Authors. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.idea.blaze.python.run.producers;
+
+import com.google.common.collect.ImmutableCollection;
+import com.google.idea.blaze.base.command.BlazeCommandName;
+import com.google.idea.blaze.base.ideinfo.TargetIdeInfo;
+import com.google.idea.blaze.base.ideinfo.TargetKey;
+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.run.BlazeCommandRunConfiguration;
+import com.google.idea.blaze.base.run.BlazeCommandRunConfigurationType;
+import com.google.idea.blaze.base.run.producers.BlazeRunConfigurationProducer;
+import com.google.idea.blaze.base.run.state.BlazeCommandRunConfigurationCommonState;
+import com.google.idea.blaze.base.sync.data.BlazeProjectDataManager;
+import com.google.idea.blaze.base.targetmaps.SourceToTargetMap;
+import com.intellij.execution.Location;
+import com.intellij.execution.actions.ConfigurationContext;
+import com.intellij.openapi.util.Ref;
+import com.intellij.openapi.util.io.FileUtil;
+import com.intellij.openapi.vfs.VirtualFile;
+import com.intellij.psi.PsiElement;
+import com.intellij.psi.PsiFile;
+import com.jetbrains.python.psi.PyFile;
+import java.io.File;
+import javax.annotation.Nullable;
+
+/** Producer for run configurations related to py_binary main classes in Blaze. */
+public class BlazePyBinaryConfigurationProducer
+    extends BlazeRunConfigurationProducer<BlazeCommandRunConfiguration> {
+
+  public BlazePyBinaryConfigurationProducer() {
+    super(BlazeCommandRunConfigurationType.getInstance());
+  }
+
+  @Override
+  protected boolean doSetupConfigFromContext(
+      BlazeCommandRunConfiguration configuration,
+      ConfigurationContext context,
+      Ref<PsiElement> sourceElement) {
+
+    Location<?> location = context.getLocation();
+    if (location == null) {
+      return false;
+    }
+    PsiElement element = location.getPsiElement();
+    PsiFile file = element.getContainingFile();
+    if (!(file instanceof PyFile)) {
+      return false;
+    }
+    Label binaryTarget = getTargetLabel(file);
+    if (binaryTarget == null) {
+      return false;
+    }
+    configuration.setTarget(binaryTarget);
+    sourceElement.set(file);
+
+    BlazeCommandRunConfigurationCommonState handlerState =
+        configuration.getHandlerStateIfType(BlazeCommandRunConfigurationCommonState.class);
+    if (handlerState == null) {
+      return false;
+    }
+    handlerState.getCommandState().setCommand(BlazeCommandName.RUN);
+    configuration.setGeneratedName();
+    return true;
+  }
+
+  @Override
+  protected boolean doIsConfigFromContext(
+      BlazeCommandRunConfiguration configuration, ConfigurationContext context) {
+
+    BlazeCommandRunConfigurationCommonState handlerState =
+        configuration.getHandlerStateIfType(BlazeCommandRunConfigurationCommonState.class);
+    if (handlerState == null
+        || !BlazeCommandName.RUN.equals(handlerState.getCommandState().getCommand())) {
+      return false;
+    }
+
+    Location<?> location = context.getLocation();
+    if (location == null) {
+      return false;
+    }
+    PsiElement element = location.getPsiElement();
+    PsiFile file = element.getContainingFile();
+    if (!(file instanceof PyFile)) {
+      return false;
+    }
+    Label binaryTarget = getTargetLabel(file);
+    if (binaryTarget == null) {
+      return false;
+    }
+    return binaryTarget.equals(configuration.getTarget());
+  }
+
+  @Nullable
+  private static Label getTargetLabel(PsiFile psiFile) {
+    VirtualFile vf = psiFile.getVirtualFile();
+    if (vf == null) {
+      return null;
+    }
+    File file = new File(vf.getPath());
+    ImmutableCollection<TargetKey> targetKeys =
+        SourceToTargetMap.getInstance(psiFile.getProject()).getRulesForSourceFile(file);
+    BlazeProjectData blazeProjectData =
+        BlazeProjectDataManager.getInstance(psiFile.getProject()).getBlazeProjectData();
+    if (blazeProjectData == null) {
+      return null;
+    }
+
+    String fileName = FileUtil.getNameWithoutExtension(file);
+    for (TargetKey key : targetKeys) {
+      TargetIdeInfo target = blazeProjectData.targetMap.get(key);
+      if (target == null) {
+        continue;
+      }
+      if (target.kind == Kind.PY_BINARY || target.kind == Kind.PY_APPENGINE_BINARY) {
+        // The 'main' attribute isn't exposed, so only suggest a binary if the name matches
+        if (key.label.targetName().toString().equals(fileName)) {
+          return key.label;
+        }
+      }
+    }
+    return null;
+  }
+}
diff --git a/python/src/com/google/idea/blaze/python/run/producers/BlazePyTestConfigurationProducer.java b/python/src/com/google/idea/blaze/python/run/producers/BlazePyTestConfigurationProducer.java
new file mode 100644
index 0000000..5d21fa8
--- /dev/null
+++ b/python/src/com/google/idea/blaze/python/run/producers/BlazePyTestConfigurationProducer.java
@@ -0,0 +1,185 @@
+/*
+ * Copyright 2017 The Bazel Authors. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.idea.blaze.python.run.producers;
+
+import com.google.common.collect.ImmutableList;
+import com.google.idea.blaze.base.command.BlazeCommandName;
+import com.google.idea.blaze.base.command.BlazeFlags;
+import com.google.idea.blaze.base.model.primitives.Label;
+import com.google.idea.blaze.base.run.BlazeCommandRunConfiguration;
+import com.google.idea.blaze.base.run.BlazeCommandRunConfigurationType;
+import com.google.idea.blaze.base.run.BlazeConfigurationNameBuilder;
+import com.google.idea.blaze.base.run.TestTargetHeuristic;
+import com.google.idea.blaze.base.run.producers.BlazeRunConfigurationProducer;
+import com.google.idea.blaze.base.run.smrunner.SmRunnerUtils;
+import com.google.idea.blaze.base.run.state.BlazeCommandRunConfigurationCommonState;
+import com.google.idea.blaze.python.run.PyTestUtils;
+import com.intellij.execution.Location;
+import com.intellij.execution.actions.ConfigurationContext;
+import com.intellij.openapi.util.Ref;
+import com.intellij.psi.PsiElement;
+import com.intellij.psi.PsiFile;
+import com.intellij.psi.util.PsiTreeUtil;
+import com.jetbrains.python.psi.PyClass;
+import com.jetbrains.python.psi.PyFile;
+import com.jetbrains.python.psi.PyFunction;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Objects;
+import javax.annotation.Nullable;
+
+/** Producer for run configurations related to python test classes in Blaze. */
+public class BlazePyTestConfigurationProducer
+    extends BlazeRunConfigurationProducer<BlazeCommandRunConfiguration> {
+
+  public BlazePyTestConfigurationProducer() {
+    super(BlazeCommandRunConfigurationType.getInstance());
+  }
+
+  private static class TestLocation {
+    @Nullable private final PyClass testClass;
+    @Nullable private final PyFunction testFunction;
+
+    private TestLocation(@Nullable PyClass testClass, @Nullable PyFunction testFunction) {
+      this.testClass = testClass;
+      this.testFunction = testFunction;
+    }
+
+    @Nullable
+    private String testFilter() {
+      if (testClass == null) {
+        return null;
+      }
+      return testFunction == null
+          ? testClass.getName()
+          : testClass.getName() + "." + testFunction.getName();
+    }
+
+    @Nullable
+    PsiElement sourceElement(PsiFile file) {
+      if (testFunction != null) {
+        return testFunction;
+      }
+      return testClass != null ? testClass : file;
+    }
+  }
+
+  /**
+   * The single selected {@link PsiElement}. Returns null if we're in a SM runner tree UI context
+   * (handled by a different producer).
+   */
+  @Nullable
+  private static PsiElement selectedPsiElement(ConfigurationContext context) {
+    List<Location<?>> selectedTestUiElements =
+        SmRunnerUtils.getSelectedSmRunnerTreeElements(context);
+    if (!selectedTestUiElements.isEmpty()) {
+      return null;
+    }
+    Location<?> location = context.getLocation();
+    return location != null ? location.getPsiElement() : null;
+  }
+
+  @Override
+  protected boolean doSetupConfigFromContext(
+      BlazeCommandRunConfiguration configuration,
+      ConfigurationContext context,
+      Ref<PsiElement> sourceElement) {
+
+    PsiElement element = selectedPsiElement(context);
+    if (element == null) {
+      return false;
+    }
+    PsiFile file = element.getContainingFile();
+    if (!(file instanceof PyFile) || !PyTestUtils.isTestFile((PyFile) file)) {
+      return false;
+    }
+    Label testTarget = TestTargetHeuristic.testTargetForPsiElement(element);
+    if (testTarget == null) {
+      return false;
+    }
+    configuration.setTarget(testTarget);
+
+    TestLocation testLocation = testLocation(element);
+    sourceElement.set(testLocation.sourceElement(file));
+    BlazeCommandRunConfigurationCommonState handlerState =
+        configuration.getHandlerStateIfType(BlazeCommandRunConfigurationCommonState.class);
+    if (handlerState == null) {
+      return false;
+    }
+    handlerState.getCommandState().setCommand(BlazeCommandName.TEST);
+
+    ImmutableList.Builder<String> flags = ImmutableList.builder();
+    String filter = testLocation.testFilter();
+    if (filter != null) {
+      flags.add(BlazeFlags.TEST_FILTER + "=" + filter);
+    }
+    // remove conflicting flags from initial configuration
+    List<String> oldFlags = new ArrayList<>(handlerState.getBlazeFlagsState().getRawFlags());
+    oldFlags.removeIf((flag) -> flag.startsWith(BlazeFlags.TEST_FILTER));
+    flags.addAll(oldFlags);
+    handlerState.getBlazeFlagsState().setRawFlags(flags.build());
+
+    BlazeConfigurationNameBuilder nameBuilder = new BlazeConfigurationNameBuilder(configuration);
+    nameBuilder.setTargetString(
+        filter != null
+            ? String.format("%s (%s)", filter, testTarget.toString())
+            : testTarget.toString());
+    configuration.setName(nameBuilder.build());
+    configuration.setNameChangedByUser(true); // don't revert to generated name
+    return true;
+  }
+
+  @Override
+  protected boolean doIsConfigFromContext(
+      BlazeCommandRunConfiguration configuration, ConfigurationContext context) {
+
+    BlazeCommandRunConfigurationCommonState handlerState =
+        configuration.getHandlerStateIfType(BlazeCommandRunConfigurationCommonState.class);
+    if (handlerState == null
+        || !BlazeCommandName.TEST.equals(handlerState.getCommandState().getCommand())) {
+      return false;
+    }
+
+    PsiElement element = selectedPsiElement(context);
+    if (element == null) {
+      return false;
+    }
+    if (!(element.getContainingFile() instanceof PyFile)) {
+      return false;
+    }
+    Label testTarget = TestTargetHeuristic.testTargetForPsiElement(element);
+    if (testTarget == null || !testTarget.equals(configuration.getTarget())) {
+      return false;
+    }
+    String filter = testLocation(element).testFilter();
+
+    String filterFlag = handlerState.getTestFilterFlag();
+    return (filterFlag == null && filter == null)
+        || Objects.equals(filterFlag, BlazeFlags.TEST_FILTER + "=" + filter);
+  }
+
+  private static TestLocation testLocation(PsiElement element) {
+    PyClass pyClass = PsiTreeUtil.getParentOfType(element, PyClass.class, false);
+    if (pyClass == null || !PyTestUtils.isTestClass(pyClass)) {
+      return new TestLocation(null, null);
+    }
+    PyFunction pyFunction = PsiTreeUtil.getParentOfType(element, PyFunction.class, false);
+    if (pyFunction != null && PyTestUtils.isTestFunction(pyFunction)) {
+      return new TestLocation(pyClass, pyFunction);
+    }
+    return new TestLocation(pyClass, null);
+  }
+}
diff --git a/python/src/com/google/idea/blaze/python/run/producers/NonBlazeProducerSuppressor.java b/python/src/com/google/idea/blaze/python/run/producers/NonBlazeProducerSuppressor.java
new file mode 100644
index 0000000..d4cabff
--- /dev/null
+++ b/python/src/com/google/idea/blaze/python/run/producers/NonBlazeProducerSuppressor.java
@@ -0,0 +1,52 @@
+/*
+ * Copyright 2017 The Bazel Authors. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.idea.blaze.python.run.producers;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.idea.blaze.base.settings.Blaze;
+import com.google.idea.sdkcompat.python.PyConfigurationProducersList;
+import com.intellij.execution.RunConfigurationProducerService;
+import com.intellij.execution.actions.RunConfigurationProducer;
+import com.intellij.openapi.components.AbstractProjectComponent;
+import com.intellij.openapi.project.Project;
+
+/** Suppresses certain non-Blaze configuration producers in Blaze projects. */
+public class NonBlazeProducerSuppressor extends AbstractProjectComponent {
+
+  public NonBlazeProducerSuppressor(Project project) {
+    super(project);
+  }
+
+  @Override
+  public void projectOpened() {
+    if (Blaze.isBlazeProject(myProject)) {
+      suppressProducers(myProject);
+    }
+  }
+
+  @VisibleForTesting
+  @SuppressWarnings("unchecked")
+  static void suppressProducers(Project project) {
+    RunConfigurationProducerService producerService =
+        RunConfigurationProducerService.getInstance(project);
+    PyConfigurationProducersList.PRODUCERS_TO_SUPPRESS.forEach(
+        (klass) -> {
+          if (RunConfigurationProducer.class.isAssignableFrom(klass)) {
+            producerService.addIgnoredProducer((Class<RunConfigurationProducer<?>>) klass);
+          }
+        });
+  }
+}
diff --git a/python/src/com/google/idea/blaze/python/run/smrunner/BlazePythonTestEventsHandler.java b/python/src/com/google/idea/blaze/python/run/smrunner/BlazePythonTestEventsHandler.java
new file mode 100644
index 0000000..f39eb76
--- /dev/null
+++ b/python/src/com/google/idea/blaze/python/run/smrunner/BlazePythonTestEventsHandler.java
@@ -0,0 +1,86 @@
+/*
+ * Copyright 2017 The Bazel Authors. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.idea.blaze.python.run.smrunner;
+
+import com.google.common.base.Joiner;
+import com.google.idea.blaze.base.command.BlazeFlags;
+import com.google.idea.blaze.base.model.primitives.Kind;
+import com.google.idea.blaze.base.run.smrunner.BlazeTestEventsHandler;
+import com.intellij.execution.Location;
+import com.intellij.execution.testframework.sm.runner.SMTestLocator;
+import com.intellij.openapi.project.Project;
+import com.intellij.psi.PsiElement;
+import com.jetbrains.python.psi.PyClass;
+import com.jetbrains.python.psi.PyFunction;
+import java.util.ArrayList;
+import java.util.EnumSet;
+import java.util.List;
+import javax.annotation.Nullable;
+
+/** Provides python-specific methods needed by the SM-runner test UI. */
+public class BlazePythonTestEventsHandler extends BlazeTestEventsHandler {
+
+  @Override
+  protected EnumSet<Kind> handledKinds() {
+    return EnumSet.of(Kind.PY_TEST);
+  }
+
+  @Override
+  public SMTestLocator getTestLocator() {
+    return BlazePythonTestLocator.INSTANCE;
+  }
+
+  @Override
+  public String testDisplayName(@Nullable Kind kind, String rawName) {
+    int lastDotIndex = rawName.lastIndexOf('.');
+    return lastDotIndex != -1 ? rawName.substring(lastDotIndex + 1) : rawName;
+  }
+
+  @Nullable
+  @Override
+  public String getTestFilter(Project project, List<Location<?>> testLocations) {
+    // python test runner parses filters of the form "class1.method1 class2.method2 ..."
+    List<String> filters = new ArrayList<>();
+    for (Location<?> location : testLocations) {
+      String filter = getFilter(location.getPsiElement());
+      if (filter != null) {
+        filters.add(filter);
+      }
+    }
+    if (filters.isEmpty()) {
+      return null;
+    }
+    return String.format("%s=%s", BlazeFlags.TEST_FILTER, Joiner.on(' ').join(filters));
+  }
+
+  @Nullable
+  private static String getFilter(PsiElement psiElement) {
+    if (psiElement instanceof PyClass) {
+      return ((PyClass) psiElement).getName();
+    }
+    if (!(psiElement instanceof PyFunction)) {
+      return null;
+    }
+    PyClass pyClass = ((PyFunction) psiElement).getContainingClass();
+    if (pyClass == null) {
+      return null;
+    }
+    String methodName = ((PyFunction) psiElement).getName();
+    String className = pyClass.getName();
+    return methodName != null && className != null ? className + "." + methodName : null;
+  }
+
+}
diff --git a/python/src/com/google/idea/blaze/python/run/smrunner/BlazePythonTestLocator.java b/python/src/com/google/idea/blaze/python/run/smrunner/BlazePythonTestLocator.java
new file mode 100644
index 0000000..5bcfdc2
--- /dev/null
+++ b/python/src/com/google/idea/blaze/python/run/smrunner/BlazePythonTestLocator.java
@@ -0,0 +1,92 @@
+/*
+ * Copyright 2017 The Bazel Authors. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.idea.blaze.python.run.smrunner;
+
+import com.google.common.collect.ImmutableList;
+import com.google.idea.blaze.base.run.smrunner.SmRunnerUtils;
+import com.google.idea.blaze.python.run.PyTestUtils;
+import com.intellij.execution.Location;
+import com.intellij.execution.PsiLocation;
+import com.intellij.execution.testframework.sm.runner.SMTestLocator;
+import com.intellij.openapi.progress.ProgressManager;
+import com.intellij.openapi.project.Project;
+import com.intellij.openapi.util.text.StringUtil;
+import com.intellij.psi.search.GlobalSearchScope;
+import com.jetbrains.python.psi.PyClass;
+import com.jetbrains.python.psi.PyFunction;
+import com.jetbrains.python.psi.stubs.PyClassNameIndex;
+import java.util.ArrayList;
+import java.util.List;
+import javax.annotation.Nullable;
+
+/** Locate python test classes / methods for test UI navigation. */
+public final class BlazePythonTestLocator implements SMTestLocator {
+
+  public static final BlazePythonTestLocator INSTANCE = new BlazePythonTestLocator();
+
+  static final String PY_TESTCASE_PREFIX = "__main__.";
+
+  private BlazePythonTestLocator() {}
+
+  @Override
+  public List<Location> getLocation(
+      String protocol, String path, Project project, GlobalSearchScope scope) {
+    if (protocol.equals(SmRunnerUtils.GENERIC_SUITE_PROTOCOL)) {
+      return findTestClass(project, scope, path);
+    }
+    if (protocol.equals(SmRunnerUtils.GENERIC_TEST_PROTOCOL)) {
+      path = StringUtil.trimStart(path, PY_TESTCASE_PREFIX);
+      String[] components = path.split("\\.");
+      if (components.length < 2) {
+        return ImmutableList.of();
+      }
+      return findTestMethod(
+          project, scope, components[components.length - 2], components[components.length - 1]);
+    }
+    return ImmutableList.of();
+  }
+
+  private static List<Location> findTestMethod(
+      Project project, GlobalSearchScope scope, String className, @Nullable String methodName) {
+    List<Location> results = new ArrayList<>();
+    if (methodName == null) {
+      return findTestClass(project, scope, className);
+    }
+    for (PyClass pyClass : PyClassNameIndex.find(className, project, scope)) {
+      ProgressManager.checkCanceled();
+      if (PyTestUtils.isTestClass(pyClass)) {
+        PyFunction method = pyClass.findMethodByName(methodName, true, null);
+        if (method != null && PyTestUtils.isTestFunction(method)) {
+          results.add(new PsiLocation<>(project, method));
+        }
+        results.add(new PsiLocation<>(project, pyClass));
+      }
+    }
+    return results;
+  }
+
+  private static List<Location> findTestClass(
+      Project project, GlobalSearchScope scope, String className) {
+    List<Location> results = new ArrayList<>();
+    for (PyClass pyClass : PyClassNameIndex.find(className, project, scope)) {
+      ProgressManager.checkCanceled();
+      if (PyTestUtils.isTestClass(pyClass)) {
+        results.add(new PsiLocation<>(project, pyClass));
+      }
+    }
+    return results;
+  }
+}
diff --git a/python/src/com/google/idea/blaze/python/search/BlazePyUseScopeEnlarger.java b/python/src/com/google/idea/blaze/python/search/BlazePyUseScopeEnlarger.java
new file mode 100644
index 0000000..908a527
--- /dev/null
+++ b/python/src/com/google/idea/blaze/python/search/BlazePyUseScopeEnlarger.java
@@ -0,0 +1,68 @@
+/*
+ * Copyright 2017 The Bazel Authors. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.idea.blaze.python.search;
+
+import com.google.idea.blaze.base.settings.Blaze;
+import com.intellij.openapi.project.Project;
+import com.intellij.openapi.vfs.VirtualFile;
+import com.intellij.psi.PsiDirectory;
+import com.intellij.psi.PsiElement;
+import com.intellij.psi.PsiFile;
+import com.intellij.psi.search.GlobalSearchScope;
+import com.intellij.psi.search.SearchScope;
+import com.intellij.psi.search.UseScopeEnlarger;
+import com.jetbrains.python.PyNames;
+import com.jetbrains.python.psi.PyFile;
+import javax.annotation.Nullable;
+
+/**
+ * Genfiles (and files in READONLY, etc.) are not included in the project scope. To resolve
+ * references to these files, we need to either add them to the project scope (which would destroy
+ * performance) or modify the default scope behavior, as done here.
+ */
+public class BlazePyUseScopeEnlarger extends UseScopeEnlarger {
+
+  @Nullable
+  @Override
+  public SearchScope getAdditionalUseScope(PsiElement element) {
+    if (!Blaze.isBlazeProject(element.getProject())) {
+      return null;
+    }
+    if (isPyPackageOutsideProject(element) || isPyFileOutsideProject(element)) {
+      return GlobalSearchScope.projectScope(element.getProject());
+    }
+    return null;
+  }
+
+  private static boolean isPyPackageOutsideProject(PsiElement element) {
+    if (!(element instanceof PsiDirectory)) {
+      return false;
+    }
+    PsiDirectory dir = (PsiDirectory) element;
+    return dir.findFile(PyNames.INIT_DOT_PY) != null
+        && !inProjectScope(dir.getProject(), dir.getVirtualFile());
+  }
+
+  private static boolean isPyFileOutsideProject(PsiElement element) {
+    PsiFile file = element.getContainingFile();
+    return file instanceof PyFile
+        && !inProjectScope(file.getProject(), file.getViewProvider().getVirtualFile());
+  }
+
+  private static boolean inProjectScope(Project project, VirtualFile virtualFile) {
+    return GlobalSearchScope.projectScope(project).contains(virtualFile);
+  }
+}
diff --git a/python/src/com/google/idea/blaze/python/sync/AlwaysPresentPythonSyncPlugin.java b/python/src/com/google/idea/blaze/python/sync/AlwaysPresentPythonSyncPlugin.java
new file mode 100644
index 0000000..7499620
--- /dev/null
+++ b/python/src/com/google/idea/blaze/python/sync/AlwaysPresentPythonSyncPlugin.java
@@ -0,0 +1,79 @@
+/*
+ * Copyright 2017 The Bazel Authors. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.idea.blaze.python.sync;
+
+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.primitives.Kind;
+import com.google.idea.blaze.base.model.primitives.LanguageClass;
+import com.google.idea.blaze.base.model.primitives.WorkspaceType;
+import com.google.idea.blaze.base.plugin.PluginUtils;
+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.projectview.ImportRoots;
+import com.google.idea.blaze.python.PythonPluginUtils;
+import com.intellij.openapi.project.Project;
+import java.util.Set;
+
+/**
+ * Unlike most of the python-specific code, will be run even if the JetBrains python plugin isn't
+ * enabled.
+ */
+public class AlwaysPresentPythonSyncPlugin extends BlazeSyncPlugin.Adapter {
+
+  @Override
+  public ImmutableList<WorkspaceType> getSupportedWorkspaceTypes() {
+    // retained for backwards-compatibility
+    return ImmutableList.of(WorkspaceType.PYTHON);
+  }
+
+  @Override
+  public Set<LanguageClass> getSupportedLanguagesInWorkspace(WorkspaceType workspaceType) {
+    // retained for backwards-compatibility
+    return ImmutableSet.of(LanguageClass.PYTHON);
+  }
+
+  @Override
+  public boolean validate(
+      Project project, BlazeContext context, BlazeProjectData blazeProjectData) {
+    ImportRoots importRoots = ImportRoots.forProjectSafe(project);
+    if (importRoots == null) {
+      return true;
+    }
+    boolean hasPythonTarget =
+        blazeProjectData
+            .targetMap
+            .targets()
+            .stream()
+            .filter(target -> importRoots.importAsSource(target.key.label))
+            .anyMatch(target -> target.kindIsOneOf(Kind.allKindsForLanguage(LanguageClass.PYTHON)));
+    if (!hasPythonTarget) {
+      return true;
+    }
+    String pluginId = PythonPluginUtils.getPythonPluginId();
+    if (!PluginUtils.isPluginEnabled(pluginId)) {
+      IssueOutput.warn(
+              "Your project appears to contain Python targets. To enable Python support, "
+                  + "install/enable the JetBrains python plugin, then restart the IDE")
+          .navigatable(PluginUtils.installOrEnablePluginNavigable(pluginId))
+          .submit(context);
+      return false;
+    }
+    return true;
+  }
+}
diff --git a/python/src/com/google/idea/blaze/python/sync/BlazePythonSyncPlugin.java b/python/src/com/google/idea/blaze/python/sync/BlazePythonSyncPlugin.java
new file mode 100644
index 0000000..2354f3c
--- /dev/null
+++ b/python/src/com/google/idea/blaze/python/sync/BlazePythonSyncPlugin.java
@@ -0,0 +1,358 @@
+/*
+ * Copyright 2017 The Bazel Authors. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.idea.blaze.python.sync;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableSet;
+import com.google.idea.blaze.base.io.VirtualFileSystemProvider;
+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.ProjectView;
+import com.google.idea.blaze.base.projectview.ProjectViewEdit;
+import com.google.idea.blaze.base.projectview.ProjectViewSet;
+import com.google.idea.blaze.base.projectview.ProjectViewSet.ProjectViewFile;
+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.output.IssueOutput;
+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.BlazeSyncPlugin;
+import com.google.idea.blaze.base.sync.GenericSourceFolderProvider;
+import com.google.idea.blaze.base.sync.SourceFolderProvider;
+import com.google.idea.blaze.base.sync.projectview.WorkspaceLanguageSettings;
+import com.google.idea.common.experiments.BoolExperiment;
+import com.google.idea.sdkcompat.transactions.Transactions;
+import com.intellij.facet.Facet;
+import com.intellij.facet.FacetManager;
+import com.intellij.facet.ModifiableFacetModel;
+import com.intellij.openapi.application.ApplicationManager;
+import com.intellij.openapi.diagnostic.Logger;
+import com.intellij.openapi.module.Module;
+import com.intellij.openapi.module.ModuleType;
+import com.intellij.openapi.project.Project;
+import com.intellij.openapi.projectRoots.Sdk;
+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.ui.Messages;
+import com.intellij.openapi.vfs.VfsUtil;
+import com.intellij.openapi.vfs.VirtualFile;
+import com.intellij.pom.NavigatableAdapter;
+import com.intellij.util.PlatformUtils;
+import com.jetbrains.python.PythonModuleTypeBase;
+import com.jetbrains.python.facet.PythonFacet;
+import com.jetbrains.python.facet.PythonFacetType;
+import com.jetbrains.python.sdk.PythonSdkType;
+import java.util.List;
+import java.util.Set;
+import javax.annotation.Nullable;
+
+/** Allows people to use a python workspace. */
+public class BlazePythonSyncPlugin extends BlazeSyncPlugin.Adapter {
+
+  private static final Logger logger = Logger.getInstance(BlazePythonSyncPlugin.class);
+
+  private static final BoolExperiment refreshExecRoot =
+      new BoolExperiment("refresh.exec.root.python", true);
+
+  @Override
+  public Set<LanguageClass> getSupportedLanguagesInWorkspace(WorkspaceType workspaceType) {
+    // supported for legacy reasons, but otherwise ignored.
+    return ImmutableSet.of(LanguageClass.PYTHON);
+  }
+
+  @Override
+  public Set<LanguageClass> getAlwaysActiveLanguages() {
+    return ImmutableSet.of(LanguageClass.PYTHON);
+  }
+
+  @Nullable
+  @Override
+  public ModuleType getWorkspaceModuleType(WorkspaceType workspaceType) {
+    // left here for backwards compatibility -- python workspace types are deprecated.
+    if (workspaceType == WorkspaceType.PYTHON && supportsPythonWorkspaceType()) {
+      return PythonModuleTypeBase.getInstance();
+    }
+    return null;
+  }
+
+  private static boolean supportsPythonWorkspaceType() {
+    return !PlatformUtils.isCLion();
+  }
+
+  @Override
+  public ImmutableList<WorkspaceType> getSupportedWorkspaceTypes() {
+    // 'supports' python by giving a more detailed error message with quick-fix later in the sync
+    // process, in the case where a python workspace type is not supported.
+    return ImmutableList.of(WorkspaceType.PYTHON);
+  }
+
+  @Nullable
+  @Override
+  public SourceFolderProvider getSourceFolderProvider(BlazeProjectData projectData) {
+    if (!projectData.workspaceLanguageSettings.isWorkspaceType(WorkspaceType.PYTHON)) {
+      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) {
+    updatePythonFacet(context, blazeProjectData, workspaceModule, workspaceModifiableModel);
+  }
+
+  @Override
+  public void refreshVirtualFileSystem(BlazeProjectData blazeProjectData) {
+    if (!refreshExecRoot.getValue()) {
+      return;
+    }
+    long start = System.currentTimeMillis();
+    refreshExecRoot(blazeProjectData);
+    long end = System.currentTimeMillis();
+    logger.info(String.format("Refreshing execution root took: %d ms", (end - start)));
+  }
+
+  private static void refreshExecRoot(BlazeProjectData blazeProjectData) {
+    // recursive refresh of the blaze execution root. This is required because our blaze aspect
+    // can't yet tell us exactly which genfiles are required to resolve the project.
+    VirtualFile execRoot =
+        VirtualFileSystemProvider.getInstance()
+            .getSystem()
+            .refreshAndFindFileByIoFile(blazeProjectData.blazeInfo.getExecutionRoot());
+    if (execRoot != null) {
+      VfsUtil.markDirtyAndRefresh(false, true, true, execRoot);
+    }
+  }
+
+  private static void updatePythonFacet(
+      BlazeContext context,
+      BlazeProjectData blazeProjectData,
+      Module workspaceModule,
+      ModifiableRootModel workspaceModifiableModel) {
+    if (!blazeProjectData.workspaceLanguageSettings.isLanguageActive(LanguageClass.PYTHON)
+        || blazeProjectData.workspaceLanguageSettings.isWorkspaceType(WorkspaceType.PYTHON)) {
+      removeFacet(workspaceModule);
+      return;
+    }
+    if (ModuleType.get(workspaceModule) instanceof PythonModuleTypeBase) {
+      return;
+    }
+    PythonFacet pythonFacet = getOrCreatePythonFacet(context, workspaceModule);
+    if (pythonFacet == null) {
+      return;
+    }
+    Library pythonLib = getFacetLibrary(pythonFacet);
+    if (pythonLib != null) {
+      workspaceModifiableModel.addLibraryEntry(pythonLib);
+    }
+  }
+
+  private static void removeFacet(Module workspaceModule) {
+    FacetManager manager = FacetManager.getInstance(workspaceModule);
+    ModifiableFacetModel facetModel = manager.createModifiableModel();
+    PythonFacet facet = manager.findFacet(PythonFacet.ID, "Python");
+    if (facet != null) {
+      facetModel.removeFacet(facet);
+      facetModel.commit();
+    }
+  }
+
+  @Nullable
+  private static PythonFacet getOrCreatePythonFacet(BlazeContext context, Module module) {
+    PythonFacet facet = findPythonFacet(module);
+    if (facet != null && facetHasSdk(facet)) {
+      return facet;
+    }
+    FacetManager manager = FacetManager.getInstance(module);
+    ModifiableFacetModel facetModel = manager.createModifiableModel();
+    if (facet != null) {
+      // we can't modify in place, IntelliJ has no hook to trigger change events. Instead we create
+      // a new facet.
+      facetModel.removeFacet(facet);
+    }
+    Sdk sdk = getOrCreatePythonSdk();
+    if (sdk == null) {
+      String msg =
+          "Unable to find a Python SDK installed.\n"
+              + "After configuring a suitable SDK in the \"Project Structure\" dialog, "
+              + "sync the project again.";
+      IssueOutput.error(msg).submit(context);
+      return null;
+    }
+    facet = manager.createFacet(PythonFacetType.getInstance(), "Python", null);
+    facetModel.addFacet(facet);
+    facetModel.commit();
+    return facet;
+  }
+
+  private static boolean facetHasSdk(PythonFacet facet) {
+    // facets aren't properly updated when SDKs change (e.g. when they're deleted), so we need to
+    // manually check against the full list.
+    Sdk sdk = facet.getConfiguration().getSdk();
+    return sdk != null && PythonSdkType.getAllSdks().contains(sdk);
+  }
+
+  @Nullable
+  private static Library getFacetLibrary(PythonFacet pythonFacet) {
+    Sdk sdk = pythonFacet.getConfiguration().getSdk();
+    if (sdk == null) {
+      return null;
+    }
+    return LibraryTablesRegistrar.getInstance()
+        .getLibraryTable()
+        .getLibraryByName(sdk.getName() + PythonFacet.PYTHON_FACET_LIBRARY_NAME_SUFFIX);
+  }
+
+  private static PythonFacet findPythonFacet(Module module) {
+    final Facet<?>[] allFacets = FacetManager.getInstance(module).getAllFacets();
+    for (Facet<?> facet : allFacets) {
+      if (facet instanceof PythonFacet) {
+        return (PythonFacet) facet;
+      }
+    }
+    return null;
+  }
+
+  @Override
+  public void updateProjectSdk(
+      Project project,
+      BlazeContext context,
+      ProjectViewSet projectViewSet,
+      BlazeVersionData blazeVersionData,
+      BlazeProjectData blazeProjectData) {
+    if (!blazeProjectData.workspaceLanguageSettings.isWorkspaceType(WorkspaceType.PYTHON)) {
+      return;
+    }
+    Sdk currentSdk = ProjectRootManager.getInstance(project).getProjectSdk();
+    if (currentSdk != null && currentSdk.getSdkType() instanceof PythonSdkType) {
+      return;
+    }
+    Sdk sdk = getOrCreatePythonSdk();
+    if (sdk != null) {
+      setProjectSdk(project, sdk);
+    }
+  }
+
+  @Nullable
+  private static Sdk getOrCreatePythonSdk() {
+    List<Sdk> sdk = PythonSdkType.getAllSdks();
+    if (!sdk.isEmpty()) {
+      return sdk.get(0);
+    }
+    return SdkConfigurationUtil.createAndAddSDK("/usr/bin/python2.7", PythonSdkType.getInstance());
+  }
+
+  private static void setProjectSdk(Project project, Sdk sdk) {
+    Transactions.submitTransactionAndWait(
+        () ->
+            ApplicationManager.getApplication()
+                .runWriteAction(() -> ProjectRootManager.getInstance(project).setProjectSdk(sdk)));
+  }
+
+  @Override
+  public boolean validateProjectView(
+      @Nullable Project project,
+      BlazeContext context,
+      ProjectViewSet projectViewSet,
+      WorkspaceLanguageSettings workspaceLanguageSettings) {
+    ProjectViewFile topLevelProjectViewFile = projectViewSet.getTopLevelProjectViewFile();
+    if (!supportsPythonWorkspaceType()
+        && workspaceLanguageSettings.isWorkspaceType(WorkspaceType.PYTHON)) {
+      String msg = "Python workspace type is not supported (and is unnecessary) for this IDE. ";
+      boolean fixable =
+          topLevelProjectViewFile != null
+              && topLevelProjectViewFile.projectView.getScalarValue(WorkspaceTypeSection.KEY)
+                  == WorkspaceType.PYTHON;
+      msg +=
+          fixable
+              ? "Click here to remove it, retaining python support"
+              : "Please remove it and resync.";
+      IssueOutput.error(msg)
+          .navigatable(
+              project == null || !fixable
+                  ? null
+                  : new NavigatableAdapter() {
+                    @Override
+                    public void navigate(boolean requestFocus) {
+                      fixLanguageSupport(project, true);
+                    }
+                  })
+          .submit(context);
+      return false;
+    }
+    return true;
+  }
+
+  private static void fixLanguageSupport(Project project, boolean removeWorkspaceType) {
+    ProjectViewEdit edit =
+        ProjectViewEdit.editLocalProjectView(
+            project,
+            builder -> {
+              if (removeWorkspaceType) {
+                removePythonWorkspaceType(builder);
+              }
+              removeFromAdditionalLanguages(builder);
+              return true;
+            });
+    if (edit == null) {
+      Messages.showErrorDialog(
+          "Could not modify project view. Check for errors in your project view and try again",
+          "Error");
+      return;
+    }
+    edit.apply();
+
+    BlazeSyncManager.getInstance(project)
+        .requestProjectSync(
+            new BlazeSyncParams.Builder("Sync", BlazeSyncParams.SyncMode.INCREMENTAL)
+                .addProjectViewTargets(true)
+                .addWorkingSet(BlazeUserSettings.getInstance().getExpandSyncToWorkingSet())
+                .build());
+  }
+
+  private static void removePythonWorkspaceType(ProjectView.Builder builder) {
+    ScalarSection<WorkspaceType> section = builder.getLast(WorkspaceTypeSection.KEY);
+    if (section != null && section.getValue() == WorkspaceType.PYTHON) {
+      builder.remove(section);
+    }
+  }
+
+  private static void removeFromAdditionalLanguages(ProjectView.Builder builder) {
+    ListSection<LanguageClass> existingSection = builder.getLast(AdditionalLanguagesSection.KEY);
+    builder.replace(
+        existingSection,
+        ListSection.update(AdditionalLanguagesSection.KEY, existingSection)
+            .remove(LanguageClass.PYTHON));
+  }
+}
diff --git a/python/src/com/google/idea/blaze/python/sync/PythonPrefetchFileSource.java b/python/src/com/google/idea/blaze/python/sync/PythonPrefetchFileSource.java
new file mode 100644
index 0000000..29bde5e
--- /dev/null
+++ b/python/src/com/google/idea/blaze/python/sync/PythonPrefetchFileSource.java
@@ -0,0 +1,40 @@
+/*
+ * Copyright 2017 The Bazel Authors. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.idea.blaze.python.sync;
+
+import com.google.common.collect.ImmutableSet;
+import com.google.idea.blaze.base.model.BlazeProjectData;
+import com.google.idea.blaze.base.prefetch.PrefetchFileSource;
+import com.google.idea.blaze.base.projectview.ProjectViewSet;
+import com.intellij.openapi.project.Project;
+import java.io.File;
+import java.util.Collection;
+import java.util.Set;
+
+/** Causes python files to become prefetched. */
+public class PythonPrefetchFileSource implements PrefetchFileSource {
+  @Override
+  public void addFilesToPrefetch(
+      Project project,
+      ProjectViewSet projectViewSet,
+      BlazeProjectData blazeProjectData,
+      Collection<File> files) {}
+
+  @Override
+  public Set<String> prefetchSrcFileExtensions() {
+    return ImmutableSet.of("py", "pyw", "pyi");
+  }
+}
diff --git a/python/tests/integrationtests/com/google/idea/blaze/python/run/producers/BlazePyBinaryConfigurationProducerTest.java b/python/tests/integrationtests/com/google/idea/blaze/python/run/producers/BlazePyBinaryConfigurationProducerTest.java
new file mode 100644
index 0000000..26e6177
--- /dev/null
+++ b/python/tests/integrationtests/com/google/idea/blaze/python/run/producers/BlazePyBinaryConfigurationProducerTest.java
@@ -0,0 +1,80 @@
+/*
+ * Copyright 2017 The Bazel Authors. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.idea.blaze.python.run.producers;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.google.idea.blaze.base.command.BlazeCommandName;
+import com.google.idea.blaze.base.ideinfo.TargetIdeInfo;
+import com.google.idea.blaze.base.ideinfo.TargetMapBuilder;
+import com.google.idea.blaze.base.model.MockBlazeProjectDataBuilder;
+import com.google.idea.blaze.base.model.MockBlazeProjectDataManager;
+import com.google.idea.blaze.base.model.primitives.TargetExpression;
+import com.google.idea.blaze.base.model.primitives.WorkspacePath;
+import com.google.idea.blaze.base.run.BlazeCommandRunConfiguration;
+import com.google.idea.blaze.base.run.producer.BlazeRunConfigurationProducerTestCase;
+import com.google.idea.blaze.base.sync.data.BlazeProjectDataManager;
+import com.intellij.execution.actions.ConfigurationContext;
+import com.intellij.execution.actions.ConfigurationFromContext;
+import com.intellij.psi.PsiFile;
+import java.util.List;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** Integration tests for {@link BlazePyBinaryConfigurationProducer}. */
+@RunWith(JUnit4.class)
+public class BlazePyBinaryConfigurationProducerTest extends BlazeRunConfigurationProducerTestCase {
+
+  @Test
+  public void testProducedFromPyFile() {
+    PsiFile pyFile =
+        createAndIndexFile(
+            new WorkspacePath("py/bin/main.py"),
+            "def main():",
+            "  return",
+            "if __name__ == '__main__':",
+            "  main()");
+
+    workspace.createFile(new WorkspacePath("py/bin/BUILD"), "py_binary(name = 'main')");
+
+    MockBlazeProjectDataBuilder builder = MockBlazeProjectDataBuilder.builder(workspaceRoot);
+    builder.setTargetMap(
+        TargetMapBuilder.builder()
+            .addTarget(
+                TargetIdeInfo.builder()
+                    .setKind("py_binary")
+                    .setLabel("//py/bin:main")
+                    .addSource(sourceRoot("py/bin/main.py"))
+                    .build())
+            .build());
+    registerProjectService(
+        BlazeProjectDataManager.class, new MockBlazeProjectDataManager(builder.build()));
+
+    ConfigurationContext context = createContextFromPsi(pyFile);
+    List<ConfigurationFromContext> configurations = context.getConfigurationsFromContext();
+    assertThat(configurations).hasSize(1);
+
+    ConfigurationFromContext fromContext = configurations.get(0);
+    assertThat(fromContext.isProducedBy(BlazePyBinaryConfigurationProducer.class)).isTrue();
+    assertThat(fromContext.getConfiguration()).isInstanceOf(BlazeCommandRunConfiguration.class);
+
+    BlazeCommandRunConfiguration config =
+        (BlazeCommandRunConfiguration) fromContext.getConfiguration();
+    assertThat(config.getTarget()).isEqualTo(TargetExpression.fromString("//py/bin:main"));
+    assertThat(getCommandType(config)).isEqualTo(BlazeCommandName.RUN);
+  }
+}
diff --git a/python/tests/integrationtests/com/google/idea/blaze/python/run/producers/BlazePyTestConfigurationProducerTest.java b/python/tests/integrationtests/com/google/idea/blaze/python/run/producers/BlazePyTestConfigurationProducerTest.java
new file mode 100644
index 0000000..7f358ac
--- /dev/null
+++ b/python/tests/integrationtests/com/google/idea/blaze/python/run/producers/BlazePyTestConfigurationProducerTest.java
@@ -0,0 +1,170 @@
+/*
+ * Copyright 2017 The Bazel Authors. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.idea.blaze.python.run.producers;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.google.idea.blaze.base.command.BlazeCommandName;
+import com.google.idea.blaze.base.ideinfo.TargetIdeInfo;
+import com.google.idea.blaze.base.ideinfo.TargetMapBuilder;
+import com.google.idea.blaze.base.lang.buildfile.psi.util.PsiUtils;
+import com.google.idea.blaze.base.model.MockBlazeProjectDataBuilder;
+import com.google.idea.blaze.base.model.MockBlazeProjectDataManager;
+import com.google.idea.blaze.base.model.primitives.TargetExpression;
+import com.google.idea.blaze.base.model.primitives.WorkspacePath;
+import com.google.idea.blaze.base.run.BlazeCommandRunConfiguration;
+import com.google.idea.blaze.base.run.producer.BlazeRunConfigurationProducerTestCase;
+import com.google.idea.blaze.base.sync.data.BlazeProjectDataManager;
+import com.intellij.execution.actions.ConfigurationContext;
+import com.intellij.execution.actions.ConfigurationFromContext;
+import com.intellij.psi.PsiFile;
+import com.jetbrains.python.psi.PyClass;
+import com.jetbrains.python.psi.PyFunction;
+import java.util.List;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/**
+ * Integration tests for {@link BlazePyTestConfigurationProducer}.
+ */
+@RunWith(JUnit4.class)
+public class BlazePyTestConfigurationProducerTest extends BlazeRunConfigurationProducerTestCase {
+
+  @Before
+  public final void suppressNativeProducers() {
+    // Project components triggered before we can set up BlazeImportSettings.
+    NonBlazeProducerSuppressor.suppressProducers(getProject());
+  }
+
+  @Test
+  public void testProducedFromPyFile() {
+    PsiFile pyFile =
+        createAndIndexFile(
+            new WorkspacePath("py/test/unittest.py"),
+            "class UnitTest(googletest.TestCase):",
+            "  def testSomething():",
+            "    return");
+
+    MockBlazeProjectDataBuilder builder = MockBlazeProjectDataBuilder.builder(workspaceRoot);
+    builder.setTargetMap(
+        TargetMapBuilder.builder()
+            .addTarget(
+                TargetIdeInfo.builder()
+                    .setKind("py_test")
+                    .setLabel("//py/test:unittests")
+                    .addSource(sourceRoot("py/test/unittest.py"))
+                    .build())
+            .build());
+    registerProjectService(
+        BlazeProjectDataManager.class, new MockBlazeProjectDataManager(builder.build()));
+
+    ConfigurationContext context = createContextFromPsi(pyFile);
+    List<ConfigurationFromContext> configurations = context.getConfigurationsFromContext();
+    assertThat(configurations).hasSize(1);
+
+    ConfigurationFromContext fromContext = configurations.get(0);
+    assertThat(fromContext.isProducedBy(BlazePyTestConfigurationProducer.class)).isTrue();
+    assertThat(fromContext.getConfiguration()).isInstanceOf(BlazeCommandRunConfiguration.class);
+
+    BlazeCommandRunConfiguration config =
+        (BlazeCommandRunConfiguration) fromContext.getConfiguration();
+    assertThat(config.getTarget()).isEqualTo(TargetExpression.fromString("//py/test:unittests"));
+    assertThat(getTestFilterContents(config)).isNull();
+    assertThat(getCommandType(config)).isEqualTo(BlazeCommandName.TEST);
+  }
+
+  @Test
+  public void testProducedFromPyClass() {
+    PsiFile pyFile =
+        createAndIndexFile(
+            new WorkspacePath("py/test/unittest.py"),
+            "class UnitTest(googletest.TestCase):",
+            "  def testSomething():",
+            "    return");
+
+    MockBlazeProjectDataBuilder builder = MockBlazeProjectDataBuilder.builder(workspaceRoot);
+    builder.setTargetMap(
+        TargetMapBuilder.builder()
+            .addTarget(
+                TargetIdeInfo.builder()
+                    .setKind("py_test")
+                    .setLabel("//py/test:unittests")
+                    .addSource(sourceRoot("py/test/unittest.py"))
+                    .build())
+            .build());
+    registerProjectService(
+        BlazeProjectDataManager.class, new MockBlazeProjectDataManager(builder.build()));
+
+    PyClass pyClass = PsiUtils.findFirstChildOfClassRecursive(pyFile, PyClass.class);
+    assertThat(pyClass).isNotNull();
+
+    ConfigurationContext context = createContextFromPsi(pyClass);
+    List<ConfigurationFromContext> configurations = context.getConfigurationsFromContext();
+    assertThat(configurations).hasSize(1);
+
+    ConfigurationFromContext fromContext = configurations.get(0);
+    assertThat(fromContext.isProducedBy(BlazePyTestConfigurationProducer.class)).isTrue();
+    assertThat(fromContext.getConfiguration()).isInstanceOf(BlazeCommandRunConfiguration.class);
+
+    BlazeCommandRunConfiguration config =
+        (BlazeCommandRunConfiguration) fromContext.getConfiguration();
+    assertThat(config.getTarget()).isEqualTo(TargetExpression.fromString("//py/test:unittests"));
+    assertThat(getTestFilterContents(config)).isEqualTo("--test_filter=UnitTest");
+    assertThat(getCommandType(config)).isEqualTo(BlazeCommandName.TEST);
+  }
+
+  @Test
+  public void testProducedFromTestCase() {
+    PsiFile pyFile =
+        createAndIndexFile(
+            new WorkspacePath("py/test/unittest.py"),
+            "class UnitTest(googletest.TestCase):",
+            "  def testSomething():",
+            "    return");
+
+    MockBlazeProjectDataBuilder builder = MockBlazeProjectDataBuilder.builder(workspaceRoot);
+    builder.setTargetMap(
+        TargetMapBuilder.builder()
+            .addTarget(
+                TargetIdeInfo.builder()
+                    .setKind("py_test")
+                    .setLabel("//py/test:unittests")
+                    .addSource(sourceRoot("py/test/unittest.py"))
+                    .build())
+            .build());
+    registerProjectService(
+        BlazeProjectDataManager.class, new MockBlazeProjectDataManager(builder.build()));
+
+    PyFunction function = PsiUtils.findFirstChildOfClassRecursive(pyFile, PyFunction.class);
+    assertThat(function).isNotNull();
+
+    ConfigurationContext context = createContextFromPsi(function);
+    List<ConfigurationFromContext> configurations = context.getConfigurationsFromContext();
+    assertThat(configurations).hasSize(1);
+
+    ConfigurationFromContext fromContext = configurations.get(0);
+    assertThat(fromContext.isProducedBy(BlazePyTestConfigurationProducer.class)).isTrue();
+    assertThat(fromContext.getConfiguration()).isInstanceOf(BlazeCommandRunConfiguration.class);
+
+    BlazeCommandRunConfiguration config =
+        (BlazeCommandRunConfiguration) fromContext.getConfiguration();
+    assertThat(config.getTarget()).isEqualTo(TargetExpression.fromString("//py/test:unittests"));
+    assertThat(getTestFilterContents(config)).isEqualTo("--test_filter=UnitTest.testSomething");
+    assertThat(getCommandType(config)).isEqualTo(BlazeCommandName.TEST);
+  }
+}
diff --git a/python/tests/integrationtests/com/google/idea/blaze/python/run/smrunner/BlazePythonTestEventsHandlerTest.java b/python/tests/integrationtests/com/google/idea/blaze/python/run/smrunner/BlazePythonTestEventsHandlerTest.java
new file mode 100644
index 0000000..c87025c
--- /dev/null
+++ b/python/tests/integrationtests/com/google/idea/blaze/python/run/smrunner/BlazePythonTestEventsHandlerTest.java
@@ -0,0 +1,106 @@
+/*
+ * Copyright 2017 The Bazel Authors. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.idea.blaze.python.run.smrunner;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.google.common.collect.Iterables;
+import com.google.idea.blaze.base.BlazeIntegrationTestCase;
+import com.google.idea.blaze.base.lang.buildfile.psi.util.PsiUtils;
+import com.google.idea.blaze.base.model.primitives.WorkspacePath;
+import com.intellij.execution.Location;
+import com.intellij.openapi.vfs.VirtualFileManager;
+import com.intellij.psi.PsiFile;
+import com.intellij.psi.search.GlobalSearchScope;
+import com.jetbrains.python.psi.PyClass;
+import com.jetbrains.python.psi.PyFunction;
+import javax.annotation.Nullable;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/**
+ * Integration tests for {@link BlazePythonTestEventsHandler}.
+ */
+@RunWith(JUnit4.class)
+public class BlazePythonTestEventsHandlerTest extends BlazeIntegrationTestCase {
+
+  private final BlazePythonTestEventsHandler handler = new BlazePythonTestEventsHandler();
+
+  @Test
+  public void testSuiteLocationResolves() {
+    PsiFile file =
+        workspace.createPsiFile(
+            new WorkspacePath("lib/app_unittest.py"),
+            "class AppUnitTest:",
+            "  def function(self):",
+            "    return");
+    PyClass pyClass = PsiUtils.findFirstChildOfClassRecursive(file, PyClass.class);
+    assertThat(pyClass).isNotNull();
+
+    String url = handler.suiteLocationUrl(null, "AppUnitTest");
+    Location<?> location = getLocation(url);
+    assertThat(location.getPsiElement()).isEqualTo(pyClass);
+  }
+
+  @Test
+  public void testFunctionLocationResolves() {
+    PsiFile file =
+        workspace.createPsiFile(
+            new WorkspacePath("lib/app_unittest.py"),
+            "class AppUnitTest:",
+            "  def testApp(self):",
+            "    return");
+    PyClass pyClass = PsiUtils.findFirstChildOfClassRecursive(file, PyClass.class);
+    PyFunction function = pyClass.findMethodByName("testApp", false, null);
+    assertThat(function).isNotNull();
+
+    String url = handler.testLocationUrl(null, null, "__main__.AppUnitTest.testApp", null);
+    Location<?> location = getLocation(url);
+    assertThat(location.getPsiElement()).isEqualTo(function);
+  }
+
+  @Test
+  public void testFunctionWithoutMainPrefixResolves() {
+    PsiFile file =
+        workspace.createPsiFile(
+            new WorkspacePath("lib/app/app_unittest.py"),
+            "class AppUnitTest:",
+            "  def testApp(self):",
+            "    return");
+    PyClass pyClass = PsiUtils.findFirstChildOfClassRecursive(file, PyClass.class);
+    PyFunction function = pyClass.findMethodByName("testApp", false, null);
+    assertThat(function).isNotNull();
+
+    String url = handler.testLocationUrl(null, null, "lib.app.AppUnitTest.testApp", null);
+    Location<?> location = getLocation(url);
+    assertThat(location.getPsiElement()).isEqualTo(function);
+  }
+
+  @Nullable
+  private Location<?> getLocation(String url) {
+    String protocol = VirtualFileManager.extractProtocol(url);
+    String path = VirtualFileManager.extractPath(url);
+    if (protocol == null) {
+      return null;
+    }
+    return Iterables.getFirst(
+        handler
+            .getTestLocator()
+            .getLocation(protocol, path, getProject(), GlobalSearchScope.allScope(getProject())),
+        null);
+  }
+}
diff --git a/python/tests/unittests/com/google/idea/blaze/python/run/BlazePyRunConfigurationRunnerTest.java b/python/tests/unittests/com/google/idea/blaze/python/run/BlazePyRunConfigurationRunnerTest.java
new file mode 100644
index 0000000..d29c2fb
--- /dev/null
+++ b/python/tests/unittests/com/google/idea/blaze/python/run/BlazePyRunConfigurationRunnerTest.java
@@ -0,0 +1,41 @@
+/*
+ * Copyright 2017 The Bazel Authors. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.idea.blaze.python.run;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.google.common.collect.ImmutableList;
+import com.google.idea.blaze.base.model.primitives.Label;
+import java.io.File;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** Unit tests for {@link BlazePyRunConfigurationRunner}. */
+@RunWith(JUnit4.class)
+public class BlazePyRunConfigurationRunnerTest {
+
+  @Test
+  public void testMultipleOutputFiles() {
+    Label target = Label.create("//path/to/package:SomeTest");
+    ImmutableList<File> outputFiles =
+        ImmutableList.of(
+            new File("blaze-bin/path/to/package/SomeTest.run.py"),
+            new File("blaze-bin/path/to/package/SomeTest"));
+    assertThat(BlazePyRunConfigurationRunner.findExecutable(target, outputFiles))
+        .isEqualTo(new File("blaze-bin/path/to/package/SomeTest"));
+  }
+}
diff --git a/scala/BUILD b/scala/BUILD
new file mode 100644
index 0000000..1343c03
--- /dev/null
+++ b/scala/BUILD
@@ -0,0 +1,97 @@
+licenses(["notice"])  # Apache 2.0
+
+load(
+    "//build_defs:build_defs.bzl",
+    "merged_plugin_xml",
+    "stamped_plugin_xml",
+    "intellij_plugin",
+    "optional_plugin_xml",
+)
+load(
+    "//testing:test_defs.bzl",
+    "intellij_unit_test_suite",
+    "intellij_integration_test_suite",
+)
+
+filegroup(
+    name = "plugin_xml",
+    srcs = ["src/META-INF/blaze-scala.xml"],
+    visibility = ["//visibility:public"],
+)
+
+optional_plugin_xml(
+    name = "optional_xml",
+    module = "org.intellij.scala",
+    plugin_xml = "src/META-INF/scala-contents.xml",
+    visibility = ["//visibility:public"],
+)
+
+merged_plugin_xml(
+    name = "merged_plugin_xml",
+    srcs = [
+        ":plugin_xml",
+        "//base:plugin_xml",
+        "//java:plugin_xml",
+    ],
+)
+
+stamped_plugin_xml(
+    name = "scala_plugin_xml",
+    plugin_id = "com.google.idea.blaze.scala",
+    plugin_name = "com.google.idea.blaze.scala",
+    plugin_xml = "merged_plugin_xml",
+)
+
+java_library(
+    name = "scala",
+    srcs = glob(["src/**/*.java"]),
+    visibility = ["//visibility:public"],
+    deps = [
+        "//base",
+        "//intellij_platform_sdk:plugin_api",
+        "//java",
+        "//third_party/scala",
+        "@jsr305_annotations//jar",
+    ],
+)
+
+intellij_plugin(
+    name = "scala_integration_test_plugin",
+    testonly = 1,
+    optional_plugin_xmls = [":optional_xml"],
+    plugin_xml = ":scala_plugin_xml",
+    deps = [":scala"],
+)
+
+intellij_unit_test_suite(
+    name = "unit_tests",
+    srcs = glob(["tests/unittests/**/*.java"]),
+    test_package_root = "com.google.idea.blaze",
+    deps = [
+        ":scala",
+        "//base",
+        "//base:unit_test_utils",
+        "//intellij_platform_sdk:plugin_api_for_tests",
+        "//java",
+        "//third_party/scala:scala_for_tests",
+        "@junit//jar",
+    ],
+)
+
+intellij_integration_test_suite(
+    name = "integration_tests",
+    srcs = glob(["tests/integrationtests/**/*.java"]),
+    required_plugins = "org.intellij.scala,com.google.idea.blaze.scala",
+    test_package_root = "com.google.idea.blaze.scala",
+    runtime_deps = [":scala_integration_test_plugin"],
+    deps = [
+        ":scala",
+        "//base",
+        "//base:integration_test_utils",
+        "//base:unit_test_utils",
+        "//intellij_platform_sdk:plugin_api_for_tests",
+        "//java",
+        "//third_party/scala:scala_for_tests",
+        "@junit//jar",
+    ],
+)
diff --git a/scala/src/META-INF/blaze-scala.xml b/scala/src/META-INF/blaze-scala.xml
new file mode 100644
index 0000000..375fdc7
--- /dev/null
+++ b/scala/src/META-INF/blaze-scala.xml
@@ -0,0 +1,5 @@
+<idea-plugin>
+  <extensions defaultExtensionNs="com.google.idea.blaze">
+    <SyncPlugin implementation="com.google.idea.blaze.scala.sync.AlwaysPresentScalaSyncPlugin"/>
+  </extensions>
+</idea-plugin>
diff --git a/scala/src/META-INF/scala-contents.xml b/scala/src/META-INF/scala-contents.xml
new file mode 100644
index 0000000..9c46cf1
--- /dev/null
+++ b/scala/src/META-INF/scala-contents.xml
@@ -0,0 +1,7 @@
+<idea-plugin>
+  <extensions defaultExtensionNs="com.google.idea.blaze">
+    <SyncPlugin implementation="com.google.idea.blaze.scala.sync.BlazeScalaSyncPlugin"/>
+    <JavaLikeLanguage
+        implementation="com.google.idea.blaze.scala.sync.source.ScalaJavaLikeLanguage"/>
+  </extensions>
+</idea-plugin>
diff --git a/scala/src/com/google/idea/blaze/scala/sync/AlwaysPresentScalaSyncPlugin.java b/scala/src/com/google/idea/blaze/scala/sync/AlwaysPresentScalaSyncPlugin.java
new file mode 100644
index 0000000..e331132
--- /dev/null
+++ b/scala/src/com/google/idea/blaze/scala/sync/AlwaysPresentScalaSyncPlugin.java
@@ -0,0 +1,63 @@
+/*
+ * Copyright 2017 The Bazel Authors. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.idea.blaze.scala.sync;
+
+import com.google.common.collect.ImmutableSet;
+import com.google.idea.blaze.base.model.primitives.LanguageClass;
+import com.google.idea.blaze.base.model.primitives.WorkspaceType;
+import com.google.idea.blaze.base.plugin.PluginUtils;
+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.projectview.WorkspaceLanguageSettings;
+import com.intellij.openapi.project.Project;
+import java.util.Set;
+import org.jetbrains.annotations.Nullable;
+
+/**
+ * Unlike most of the scala-specific code, will be run even if the JetBrains scala plugin isn't
+ * enabled.
+ */
+public class AlwaysPresentScalaSyncPlugin extends BlazeSyncPlugin.Adapter {
+  private static final String SCALA_PLUGIN_ID = "org.intellij.scala";
+
+  @Override
+  public Set<LanguageClass> getSupportedLanguagesInWorkspace(WorkspaceType workspaceType) {
+    if (workspaceType.equals(WorkspaceType.JAVA)) {
+      return ImmutableSet.of(LanguageClass.SCALA);
+    }
+    return ImmutableSet.of();
+  }
+
+  @Override
+  public boolean validateProjectView(
+      @Nullable Project project,
+      BlazeContext context,
+      ProjectViewSet projectViewSet,
+      WorkspaceLanguageSettings workspaceLanguageSettings) {
+    if (!workspaceLanguageSettings.isLanguageActive(LanguageClass.SCALA)) {
+      return true;
+    }
+    if (!PluginUtils.isPluginEnabled(SCALA_PLUGIN_ID)) {
+      IssueOutput.error("Scala plugin needed for Scala language support.")
+          .navigatable(PluginUtils.installOrEnablePluginNavigable(SCALA_PLUGIN_ID))
+          .submit(context);
+      return false;
+    }
+    return true;
+  }
+}
diff --git a/scala/src/com/google/idea/blaze/scala/sync/BlazeScalaLibrarySource.java b/scala/src/com/google/idea/blaze/scala/sync/BlazeScalaLibrarySource.java
new file mode 100644
index 0000000..86f276f
--- /dev/null
+++ b/scala/src/com/google/idea/blaze/scala/sync/BlazeScalaLibrarySource.java
@@ -0,0 +1,41 @@
+/*
+ * Copyright 2017 The Bazel Authors. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.idea.blaze.scala.sync;
+
+import com.google.common.collect.ImmutableList;
+import com.google.idea.blaze.base.model.BlazeLibrary;
+import com.google.idea.blaze.base.model.BlazeProjectData;
+import com.google.idea.blaze.base.sync.libraries.LibrarySource;
+import com.google.idea.blaze.scala.sync.model.BlazeScalaSyncData;
+import java.util.Collection;
+
+/** Provides libraries required by Scala rules. */
+public class BlazeScalaLibrarySource extends LibrarySource.Adapter {
+  private final BlazeProjectData blazeProjectData;
+
+  BlazeScalaLibrarySource(BlazeProjectData blazeProjectData) {
+    this.blazeProjectData = blazeProjectData;
+  }
+
+  @Override
+  public Collection<? extends BlazeLibrary> getLibraries() {
+    BlazeScalaSyncData syncData = blazeProjectData.syncState.get(BlazeScalaSyncData.class);
+    if (syncData == null) {
+      return ImmutableList.of();
+    }
+    return syncData.importResult.libraries.values();
+  }
+}
diff --git a/scala/src/com/google/idea/blaze/scala/sync/BlazeScalaSyncPlugin.java b/scala/src/com/google/idea/blaze/scala/sync/BlazeScalaSyncPlugin.java
new file mode 100644
index 0000000..20fa54c
--- /dev/null
+++ b/scala/src/com/google/idea/blaze/scala/sync/BlazeScalaSyncPlugin.java
@@ -0,0 +1,128 @@
+/*
+ * Copyright 2017 The Bazel Authors. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.idea.blaze.scala.sync;
+
+import com.google.common.collect.ImmutableSet;
+import com.google.idea.blaze.base.command.info.BlazeInfo;
+import com.google.idea.blaze.base.ideinfo.TargetMap;
+import com.google.idea.blaze.base.model.BlazeProjectData;
+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.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.Scope;
+import com.google.idea.blaze.base.scope.scopes.TimingScope;
+import com.google.idea.blaze.base.sync.BlazeSyncPlugin;
+import com.google.idea.blaze.base.sync.libraries.LibrarySource;
+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.WorkingSet;
+import com.google.idea.blaze.base.sync.workspace.WorkspacePathResolver;
+import com.google.idea.blaze.scala.sync.importer.BlazeScalaWorkspaceImporter;
+import com.google.idea.blaze.scala.sync.model.BlazeScalaImportResult;
+import com.google.idea.blaze.scala.sync.model.BlazeScalaSyncData;
+import com.intellij.openapi.module.Module;
+import com.intellij.openapi.project.Project;
+import com.intellij.openapi.roots.ModifiableRootModel;
+import com.intellij.openapi.roots.OrderEnumerator;
+import com.intellij.openapi.roots.ui.configuration.libraryEditor.ExistingLibraryEditor;
+import java.util.Set;
+import javax.annotation.Nullable;
+import org.jetbrains.plugins.scala.project.ScalaLibraryType;
+
+/** Supports scala. */
+public class BlazeScalaSyncPlugin extends BlazeSyncPlugin.Adapter {
+  @Override
+  public Set<LanguageClass> getSupportedLanguagesInWorkspace(WorkspaceType workspaceType) {
+    if (workspaceType.equals(WorkspaceType.JAVA)) {
+      return ImmutableSet.of(LanguageClass.SCALA);
+    }
+    return ImmutableSet.of();
+  }
+
+  @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.SCALA)) {
+      return;
+    }
+    OrderEnumerator.orderEntries(workspaceModule)
+        .forEachLibrary(
+            library -> {
+              // Convert the type of the SDK library to prevent the scala plugin from
+              // showing the missing SDK notification.
+              // TODO: use a canonical class in the SDK (e.g., scala.App) instead of the name?
+              if (library.getName() != null && library.getName().startsWith("scala-library")) {
+                ExistingLibraryEditor editor = new ExistingLibraryEditor(library, null);
+                editor.setType(ScalaLibraryType.instance());
+                editor.commit();
+                return false; // stop
+              }
+              return true; // continue
+            });
+  }
+
+  @Override
+  public void updateSyncState(
+      Project project,
+      BlazeContext context,
+      WorkspaceRoot workspaceRoot,
+      ProjectViewSet projectViewSet,
+      WorkspaceLanguageSettings workspaceLanguageSettings,
+      BlazeInfo blazeInfo,
+      @Nullable WorkingSet workingSet,
+      WorkspacePathResolver workspacePathResolver,
+      ArtifactLocationDecoder artifactLocationDecoder,
+      TargetMap targetMap,
+      Builder syncStateBuilder,
+      @Nullable SyncState previousSyncState) {
+    if (!workspaceLanguageSettings.isLanguageActive(LanguageClass.SCALA)) {
+      return;
+    }
+    BlazeScalaWorkspaceImporter blazeScalaWorkspaceImporter =
+        new BlazeScalaWorkspaceImporter(project, workspaceRoot, projectViewSet, targetMap);
+    BlazeScalaImportResult importResult =
+        Scope.push(
+            context,
+            (childContext) -> {
+              childContext.push(new TimingScope("ScalaWorkspaceImporter"));
+              return blazeScalaWorkspaceImporter.importWorkspace();
+            });
+    BlazeScalaSyncData syncData = new BlazeScalaSyncData(importResult);
+    syncStateBuilder.put(BlazeScalaSyncData.class, syncData);
+  }
+
+  @Nullable
+  @Override
+  public LibrarySource getLibrarySource(
+      ProjectViewSet projectViewSet, BlazeProjectData blazeProjectData) {
+    if (!blazeProjectData.workspaceLanguageSettings.isLanguageActive(LanguageClass.SCALA)) {
+      return null;
+    }
+    return new BlazeScalaLibrarySource(blazeProjectData);
+  }
+}
diff --git a/scala/src/com/google/idea/blaze/scala/sync/importer/BlazeScalaWorkspaceImporter.java b/scala/src/com/google/idea/blaze/scala/sync/importer/BlazeScalaWorkspaceImporter.java
new file mode 100644
index 0000000..5410f2c
--- /dev/null
+++ b/scala/src/com/google/idea/blaze/scala/sync/importer/BlazeScalaWorkspaceImporter.java
@@ -0,0 +1,96 @@
+/*
+ * Copyright 2017 The Bazel Authors. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.idea.blaze.scala.sync.importer;
+
+import com.google.common.collect.ImmutableMap;
+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.LibraryKey;
+import com.google.idea.blaze.base.model.primitives.Kind;
+import com.google.idea.blaze.base.model.primitives.LanguageClass;
+import com.google.idea.blaze.base.model.primitives.WorkspaceRoot;
+import com.google.idea.blaze.base.projectview.ProjectViewSet;
+import com.google.idea.blaze.base.sync.projectview.ProjectViewTargetImportFilter;
+import com.google.idea.blaze.base.targetmaps.TransitiveDependencyMap;
+import com.google.idea.blaze.java.sync.model.BlazeJarLibrary;
+import com.google.idea.blaze.scala.sync.model.BlazeScalaImportResult;
+import com.intellij.openapi.project.Project;
+import java.util.Collection;
+import java.util.List;
+import java.util.stream.Collectors;
+
+/** Builds a BlazeWorkspace. */
+public final class BlazeScalaWorkspaceImporter {
+  private final Project project;
+  private final WorkspaceRoot workspaceRoot;
+  private final ProjectViewSet projectViewSet;
+  private final TargetMap targetMap;
+
+  public BlazeScalaWorkspaceImporter(
+      Project project,
+      WorkspaceRoot workspaceRoot,
+      ProjectViewSet projectViewSet,
+      TargetMap targetMap) {
+    this.project = project;
+    this.workspaceRoot = workspaceRoot;
+    this.projectViewSet = projectViewSet;
+    this.targetMap = targetMap;
+  }
+
+  public BlazeScalaImportResult importWorkspace() {
+    ProjectViewTargetImportFilter importFilter =
+        new ProjectViewTargetImportFilter(project, workspaceRoot, projectViewSet);
+
+    Collection<Kind> scalaKinds = Kind.allKindsForLanguage(LanguageClass.SCALA);
+    List<TargetKey> scalaSourceTargets =
+        targetMap
+            .targets()
+            .stream()
+            .filter(target -> target.javaIdeInfo != null)
+            .filter(target -> target.kindIsOneOf(scalaKinds))
+            .filter(importFilter::isSourceTarget)
+            .map(target -> target.key)
+            .collect(Collectors.toList());
+
+    ImmutableMap.Builder<LibraryKey, BlazeJarLibrary> libraries = ImmutableMap.builder();
+
+    // Add every jar in the transitive closure of dependencies.
+    // Direct dependencies of the working set will be double counted by BlazeJavaWorkspaceImporter,
+    // but since they'll all merged into one set, we will end up with exactly one of each.
+    for (TargetKey dependency :
+        TransitiveDependencyMap.getTransitiveDependencies(scalaSourceTargets, targetMap)) {
+      TargetIdeInfo target = targetMap.get(dependency);
+      if (target == null) {
+        continue;
+      }
+      // Except source targets.
+      if (importFilter.isSourceTarget(target)) {
+        continue;
+      }
+      if (target.javaIdeInfo != null) {
+        target
+            .javaIdeInfo
+            .jars
+            .stream()
+            .map(BlazeJarLibrary::new)
+            .forEach(library -> libraries.put(library.key, library));
+      }
+    }
+
+    return new BlazeScalaImportResult(libraries.build());
+  }
+}
diff --git a/scala/src/com/google/idea/blaze/scala/sync/model/BlazeScalaImportResult.java b/scala/src/com/google/idea/blaze/scala/sync/model/BlazeScalaImportResult.java
new file mode 100644
index 0000000..2140c19
--- /dev/null
+++ b/scala/src/com/google/idea/blaze/scala/sync/model/BlazeScalaImportResult.java
@@ -0,0 +1,34 @@
+/*
+ * Copyright 2017 The Bazel Authors. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.idea.blaze.scala.sync.model;
+
+import com.google.common.collect.ImmutableMap;
+import com.google.idea.blaze.base.model.LibraryKey;
+import com.google.idea.blaze.java.sync.model.BlazeJarLibrary;
+import java.io.Serializable;
+import javax.annotation.concurrent.Immutable;
+
+/** The result of a blaze import operation. */
+@Immutable
+public class BlazeScalaImportResult implements Serializable {
+  private static final long serialVersionUID = 1L;
+
+  public final ImmutableMap<LibraryKey, BlazeJarLibrary> libraries;
+
+  public BlazeScalaImportResult(ImmutableMap<LibraryKey, BlazeJarLibrary> libraries) {
+    this.libraries = libraries;
+  }
+}
diff --git a/scala/src/com/google/idea/blaze/scala/sync/model/BlazeScalaSyncData.java b/scala/src/com/google/idea/blaze/scala/sync/model/BlazeScalaSyncData.java
new file mode 100644
index 0000000..56ce799
--- /dev/null
+++ b/scala/src/com/google/idea/blaze/scala/sync/model/BlazeScalaSyncData.java
@@ -0,0 +1,29 @@
+/*
+ * Copyright 2017 The Bazel Authors. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.idea.blaze.scala.sync.model;
+
+import java.io.Serializable;
+
+/** Sync data for the scala plugin. */
+public class BlazeScalaSyncData implements Serializable {
+  private static final long serialVersionUID = 1L;
+
+  public final BlazeScalaImportResult importResult;
+
+  public BlazeScalaSyncData(BlazeScalaImportResult importResult) {
+    this.importResult = importResult;
+  }
+}
diff --git a/scala/src/com/google/idea/blaze/scala/sync/source/ScalaJavaLikeLanguage.java b/scala/src/com/google/idea/blaze/scala/sync/source/ScalaJavaLikeLanguage.java
new file mode 100644
index 0000000..df37840
--- /dev/null
+++ b/scala/src/com/google/idea/blaze/scala/sync/source/ScalaJavaLikeLanguage.java
@@ -0,0 +1,28 @@
+/*
+ * Copyright 2017 The Bazel Authors. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.idea.blaze.scala.sync.source;
+
+import com.google.common.collect.ImmutableSet;
+import com.google.idea.blaze.java.sync.source.JavaLikeLanguage;
+import java.util.Set;
+
+/** Provides Java-like parts of Scala to the Java plugin. */
+public class ScalaJavaLikeLanguage implements JavaLikeLanguage {
+  @Override
+  public Set<String> getFileExtensions() {
+    return ImmutableSet.of(".scala");
+  }
+}
diff --git a/scala/tests/integrationtests/com/google/idea/blaze/scala/sync/ScalaSyncTest.java b/scala/tests/integrationtests/com/google/idea/blaze/scala/sync/ScalaSyncTest.java
new file mode 100644
index 0000000..c55075f
--- /dev/null
+++ b/scala/tests/integrationtests/com/google/idea/blaze/scala/sync/ScalaSyncTest.java
@@ -0,0 +1,165 @@
+/*
+ * Copyright 2017 The Bazel Authors. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.idea.blaze.scala.sync;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.google.common.collect.ImmutableSet;
+import com.google.idea.blaze.base.ideinfo.JavaIdeInfo;
+import com.google.idea.blaze.base.ideinfo.TargetIdeInfo;
+import com.google.idea.blaze.base.ideinfo.TargetMap;
+import com.google.idea.blaze.base.ideinfo.TargetMapBuilder;
+import com.google.idea.blaze.base.model.BlazeProjectData;
+import com.google.idea.blaze.base.model.primitives.LanguageClass;
+import com.google.idea.blaze.base.model.primitives.WorkspacePath;
+import com.google.idea.blaze.base.model.primitives.WorkspaceType;
+import com.google.idea.blaze.base.sync.BlazeSyncIntegrationTestCase;
+import com.google.idea.blaze.base.sync.BlazeSyncParams;
+import com.google.idea.blaze.base.sync.BlazeSyncParams.SyncMode;
+import com.google.idea.blaze.base.sync.data.BlazeProjectDataManager;
+import com.google.idea.blaze.base.sync.projectview.WorkspaceLanguageSettings;
+import com.google.idea.blaze.java.sync.model.BlazeContentEntry;
+import com.google.idea.blaze.java.sync.model.BlazeJavaSyncData;
+import com.google.idea.blaze.java.sync.model.BlazeSourceDirectory;
+import java.util.List;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** Scala-specific sync integration tests. */
+@RunWith(JUnit4.class)
+public class ScalaSyncTest extends BlazeSyncIntegrationTestCase {
+  @Test
+  public void testScalaClassesPresentInClassPath() throws Exception {
+    setProjectView(
+        "directories:",
+        "  src/main/scala/com/google",
+        "targets:",
+        "  //src/main/scala/com/google:lib",
+        "additional_languages:",
+        "  scala");
+
+    workspace.createFile(
+        new WorkspacePath("src/main/scala/com/google/ClassWithUniqueName1.scala"),
+        "package com.google;",
+        "public class ClassWithUniqueName1 {}");
+
+    workspace.createFile(
+        new WorkspacePath("src/main/scala/com/google/ClassWithUniqueName2.scala"),
+        "package com.google;",
+        "public class ClassWithUniqueName2 {}");
+
+    TargetMap targetMap =
+        TargetMapBuilder.builder()
+            .addTarget(
+                TargetIdeInfo.builder()
+                    .setBuildFile(sourceRoot("src/main/scala/com/google/BUILD"))
+                    .setLabel("//src/main/scala/com/google:lib")
+                    .setKind("scala_library")
+                    .addSource(sourceRoot("src/main/scala/com/google/ClassWithUniqueName1.scala"))
+                    .addSource(sourceRoot("src/main/scala/com/google/ClassWithUniqueName2.scala"))
+                    .setJavaInfo(JavaIdeInfo.builder()))
+            .build();
+
+    setTargetMap(targetMap);
+
+    BlazeSyncParams syncParams =
+        new BlazeSyncParams.Builder("Full Sync", BlazeSyncParams.SyncMode.FULL)
+            .addProjectViewTargets(true)
+            .build();
+    runBlazeSync(syncParams);
+
+    errorCollector.assertNoIssues();
+
+    BlazeProjectData blazeProjectData =
+        BlazeProjectDataManager.getInstance(getProject()).getBlazeProjectData();
+    assertThat(blazeProjectData).isNotNull();
+    assertThat(blazeProjectData.targetMap).isEqualTo(targetMap);
+    assertThat(blazeProjectData.workspaceLanguageSettings)
+        .isEqualTo(
+            new WorkspaceLanguageSettings(
+                WorkspaceType.JAVA,
+                ImmutableSet.of(LanguageClass.GENERIC, LanguageClass.JAVA, LanguageClass.SCALA)));
+
+    BlazeJavaSyncData javaSyncData = blazeProjectData.syncState.get(BlazeJavaSyncData.class);
+    assertThat(javaSyncData).isNotNull();
+    List<BlazeContentEntry> contentEntries = javaSyncData.importResult.contentEntries;
+    assertThat(contentEntries).hasSize(1);
+
+    BlazeContentEntry contentEntry = contentEntries.get(0);
+    assertThat(contentEntry.contentRoot.getPath())
+        .isEqualTo(
+            workspaceRoot.fileForPath(new WorkspacePath("src/main/scala/com/google")).getPath());
+    assertThat(contentEntry.sources).hasSize(1);
+
+    BlazeSourceDirectory sourceDir = contentEntry.sources.get(0);
+    assertThat(sourceDir.getPackagePrefix()).isEqualTo("com.google");
+    assertThat(sourceDir.getDirectory().getPath())
+        .isEqualTo(
+            workspaceRoot.fileForPath(new WorkspacePath("src/main/scala/com/google")).getPath());
+  }
+
+  @Test
+  public void testSimpleSync() throws Exception {
+    setProjectView(
+        "directories:",
+        "  src/main/scala/com/google",
+        "targets:",
+        "  //src/main/scala/com/google:lib",
+        "additional_languages:",
+        "  scala");
+
+    workspace.createFile(
+        new WorkspacePath("src/main/scala/com/google/Source.scala"),
+        "package com.google;",
+        "public class Source {}");
+
+    workspace.createFile(
+        new WorkspacePath("src/main/scala/com/google/Other.scala"),
+        "package com.google;",
+        "public class Other {}");
+
+    TargetMap targetMap =
+        TargetMapBuilder.builder()
+            .addTarget(
+                TargetIdeInfo.builder()
+                    .setBuildFile(sourceRoot("src/main/scala/com/google/BUILD"))
+                    .setLabel("//src/main/scala/com/google:lib")
+                    .setKind("scala_library")
+                    .addSource(sourceRoot("src/main/scala/com/google/Source.scala"))
+                    .addSource(sourceRoot("src/main/scala/com/google/Other.scala")))
+            .build();
+
+    setTargetMap(targetMap);
+
+    runBlazeSync(
+        new BlazeSyncParams.Builder("Sync", SyncMode.INCREMENTAL)
+            .addProjectViewTargets(true)
+            .build());
+
+    errorCollector.assertNoIssues();
+
+    BlazeProjectData blazeProjectData =
+        BlazeProjectDataManager.getInstance(getProject()).getBlazeProjectData();
+    assertThat(blazeProjectData).isNotNull();
+    assertThat(blazeProjectData.targetMap).isEqualTo(targetMap);
+    assertThat(blazeProjectData.workspaceLanguageSettings)
+        .isEqualTo(
+            new WorkspaceLanguageSettings(
+                WorkspaceType.JAVA,
+                ImmutableSet.of(LanguageClass.GENERIC, LanguageClass.SCALA, LanguageClass.JAVA)));
+  }
+}
diff --git a/scala/tests/unittests/com/google/idea/blaze/java/sync/source/ScalaSourceDirectoryCalculatorTest.java b/scala/tests/unittests/com/google/idea/blaze/java/sync/source/ScalaSourceDirectoryCalculatorTest.java
new file mode 100644
index 0000000..c364c85
--- /dev/null
+++ b/scala/tests/unittests/com/google/idea/blaze/java/sync/source/ScalaSourceDirectoryCalculatorTest.java
@@ -0,0 +1,267 @@
+/*
+ * Copyright 2017 The Bazel Authors. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.idea.blaze.java.sync.source;
+
+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.BlazeTestCase;
+import com.google.idea.blaze.base.ideinfo.ArtifactLocation;
+import com.google.idea.blaze.base.ideinfo.TargetKey;
+import com.google.idea.blaze.base.io.InputStreamProvider;
+import com.google.idea.blaze.base.model.primitives.Label;
+import com.google.idea.blaze.base.model.primitives.WorkspacePath;
+import com.google.idea.blaze.base.model.primitives.WorkspaceRoot;
+import com.google.idea.blaze.base.prefetch.MockPrefetchService;
+import com.google.idea.blaze.base.prefetch.PrefetchService;
+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.workspace.ArtifactLocationDecoder;
+import com.google.idea.blaze.java.sync.model.BlazeContentEntry;
+import com.google.idea.blaze.java.sync.model.BlazeSourceDirectory;
+import com.google.idea.blaze.scala.sync.source.ScalaJavaLikeLanguage;
+import com.intellij.openapi.extensions.ExtensionPoint;
+import com.intellij.util.containers.HashMap;
+import java.io.ByteArrayInputStream;
+import java.io.File;
+import java.io.InputStream;
+import java.io.UnsupportedEncodingException;
+import java.util.List;
+import java.util.Map;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** Test cases for {@link SourceDirectoryCalculator} with Scala sources. */
+@RunWith(JUnit4.class)
+public class ScalaSourceDirectoryCalculatorTest extends BlazeTestCase {
+  private MockInputStreamProvider mockInputStreamProvider;
+  private SourceDirectoryCalculator sourceDirectoryCalculator;
+  private final BlazeContext context = new BlazeContext();
+  private final ErrorCollector issues = new ErrorCollector();
+  private final WorkspaceRoot workspaceRoot = new WorkspaceRoot(new File("/root"));
+
+  private final ArtifactLocationDecoder decoder =
+      (ArtifactLocationDecoder)
+          artifactLocation -> new File("/root", artifactLocation.getRelativePath());
+
+  @Override
+  protected void initTest(
+      @NotNull Container applicationServices, @NotNull Container projectServices) {
+    super.initTest(applicationServices, projectServices);
+
+    mockInputStreamProvider = new MockInputStreamProvider();
+    applicationServices.register(InputStreamProvider.class, mockInputStreamProvider);
+    applicationServices.register(JavaSourcePackageReader.class, new JavaSourcePackageReader());
+    applicationServices.register(PackageManifestReader.class, new PackageManifestReader());
+    applicationServices.register(PrefetchService.class, new MockPrefetchService());
+
+    ExtensionPoint<JavaLikeLanguage> javaLikeLanguages =
+        registerExtensionPoint(JavaLikeLanguage.EP_NAME, JavaLikeLanguage.class);
+    javaLikeLanguages.registerExtension(new JavaLikeLanguage.Java());
+    javaLikeLanguages.registerExtension(new ScalaJavaLikeLanguage());
+
+    context.addOutputSink(IssueOutput.class, issues);
+    sourceDirectoryCalculator = new SourceDirectoryCalculator();
+  }
+
+  @Test
+  public void testSingleScalaSource() {
+    mockInputStreamProvider.addFile(
+        "/root/src/main/scala/com/google/Foo.scala", "package com.google\n public class Foo {}");
+    List<SourceArtifact> sourceArtifacts =
+        ImmutableList.of(
+            SourceArtifact.builder(
+                    TargetKey.forPlainTarget(Label.create("//src/main/scala/com/google:foo")))
+                .setArtifactLocation(
+                    ArtifactLocation.builder()
+                        .setRelativePath("src/main/scala/com/google/Foo.scala")
+                        .setIsSource(true))
+                .build());
+    ImmutableList<BlazeContentEntry> result =
+        sourceDirectoryCalculator.calculateContentEntries(
+            project,
+            context,
+            workspaceRoot,
+            decoder,
+            ImmutableList.of(new WorkspacePath("src/main/scala/com/google")),
+            sourceArtifacts,
+            ImmutableMap.of());
+    assertThat(result)
+        .containsExactly(
+            BlazeContentEntry.builder("/root/src/main/scala/com/google")
+                .addSource(
+                    BlazeSourceDirectory.builder("/root/src/main/scala/com/google")
+                        .setPackagePrefix("com.google")
+                        .build())
+                .build());
+    issues.assertNoIssues();
+  }
+
+  @Test
+  public void testMultipleScalaSources() {
+    mockInputStreamProvider
+        .addFile(
+            "/root/src/main/scala/com/google/Foo.scala",
+            "package com.google;\n public class Foo {}")
+        .addFile(
+            "/root/src/main/scala/com/google/Bar.scala", "package com.google\n public class Bar {}")
+        .addFile(
+            "/root/src/main/scala/com/alphabet/Baz.scala",
+            "package com.alphabet {\n public class Baz {} }");
+    List<SourceArtifact> sourceArtifacts =
+        ImmutableList.of(
+            SourceArtifact.builder(
+                    TargetKey.forPlainTarget(Label.create("//src/main/scala/com/google:foo")))
+                .setArtifactLocation(
+                    ArtifactLocation.builder()
+                        .setRelativePath("src/main/scala/com/google/Foo.scala")
+                        .setIsSource(true))
+                .build(),
+            SourceArtifact.builder(
+                    TargetKey.forPlainTarget(Label.create("//src/main/scala/com/google:bar")))
+                .setArtifactLocation(
+                    ArtifactLocation.builder()
+                        .setRelativePath("src/main/scala/com/google/Bar.scala")
+                        .setIsSource(true))
+                .build(),
+            SourceArtifact.builder(
+                    TargetKey.forPlainTarget(Label.create("//src/main/scala/com/alphabet:baz")))
+                .setArtifactLocation(
+                    ArtifactLocation.builder()
+                        .setRelativePath("src/main/scala/com/alphabet/Baz.scala")
+                        .setIsSource(true))
+                .build());
+    ImmutableList<BlazeContentEntry> result =
+        sourceDirectoryCalculator.calculateContentEntries(
+            project,
+            context,
+            workspaceRoot,
+            decoder,
+            ImmutableList.of(
+                new WorkspacePath("src/main/scala/com/google"),
+                new WorkspacePath("src/main/scala/com/alphabet")),
+            sourceArtifacts,
+            ImmutableMap.of());
+    assertThat(result)
+        .containsExactly(
+            BlazeContentEntry.builder("/root/src/main/scala/com/google")
+                .addSource(
+                    BlazeSourceDirectory.builder("/root/src/main/scala/com/google")
+                        .setPackagePrefix("com.google")
+                        .build())
+                .build(),
+            BlazeContentEntry.builder("/root/src/main/scala/com/alphabet")
+                .addSource(
+                    BlazeSourceDirectory.builder("/root/src/main/scala/com/alphabet")
+                        .setPackagePrefix("com.alphabet")
+                        .build())
+                .build());
+    issues.assertNoIssues();
+  }
+
+  @Test
+  public void testMixedScalaAndJavaSources() {
+    mockInputStreamProvider
+        .addFile(
+            "/root/src/main/java/com/google/Foo.java", "package com.google;\n public class Foo {}")
+        .addFile(
+            "/root/src/main/scala/com/google/Bar.scala", "package com.google\n public class Bar {}")
+        .addFile(
+            "/root/src/main/scala/com/alphabet/Baz.scala",
+            "package com.alphabet {\n public class Baz {} }");
+    List<SourceArtifact> sourceArtifacts =
+        ImmutableList.of(
+            SourceArtifact.builder(
+                    TargetKey.forPlainTarget(Label.create("//src/main/java/com/google:foo")))
+                .setArtifactLocation(
+                    ArtifactLocation.builder()
+                        .setRelativePath("src/main/java/com/google/Foo.java")
+                        .setIsSource(true))
+                .build(),
+            SourceArtifact.builder(
+                    TargetKey.forPlainTarget(Label.create("//src/main/scala/com/google:bar")))
+                .setArtifactLocation(
+                    ArtifactLocation.builder()
+                        .setRelativePath("src/main/scala/com/google/Bar.scala")
+                        .setIsSource(true))
+                .build(),
+            SourceArtifact.builder(
+                    TargetKey.forPlainTarget(Label.create("//src/main/scala/com/alphabet:baz")))
+                .setArtifactLocation(
+                    ArtifactLocation.builder()
+                        .setRelativePath("src/main/scala/com/alphabet/Baz.scala")
+                        .setIsSource(true))
+                .build());
+    ImmutableList<BlazeContentEntry> result =
+        sourceDirectoryCalculator.calculateContentEntries(
+            project,
+            context,
+            workspaceRoot,
+            decoder,
+            ImmutableList.of(
+                new WorkspacePath("src/main/java/com/google"),
+                new WorkspacePath("src/main/scala/com/google"),
+                new WorkspacePath("src/main/scala/com/alphabet")),
+            sourceArtifacts,
+            ImmutableMap.of());
+    assertThat(result)
+        .containsExactly(
+            BlazeContentEntry.builder("/root/src/main/java/com/google")
+                .addSource(
+                    BlazeSourceDirectory.builder("/root/src/main/java/com/google")
+                        .setPackagePrefix("com.google")
+                        .build())
+                .build(),
+            BlazeContentEntry.builder("/root/src/main/scala/com/google")
+                .addSource(
+                    BlazeSourceDirectory.builder("/root/src/main/scala/com/google")
+                        .setPackagePrefix("com.google")
+                        .build())
+                .build(),
+            BlazeContentEntry.builder("/root/src/main/scala/com/alphabet")
+                .addSource(
+                    BlazeSourceDirectory.builder("/root/src/main/scala/com/alphabet")
+                        .setPackagePrefix("com.alphabet")
+                        .build())
+                .build());
+    issues.assertNoIssues();
+  }
+
+  private static class MockInputStreamProvider implements InputStreamProvider {
+
+    private final Map<String, InputStream> files = new HashMap<>();
+
+    MockInputStreamProvider addFile(String filePath, String src) {
+      try {
+        files.put(filePath, new ByteArrayInputStream(src.getBytes("UTF-8")));
+      } catch (UnsupportedEncodingException ignored) {
+        // ignored
+      }
+      return this;
+    }
+
+    @Nullable
+    @Override
+    public InputStream getFile(@NotNull File path) {
+      return files.get(path.getPath());
+    }
+  }
+}
diff --git a/scala/tests/unittests/com/google/idea/blaze/scala/sync/BlazeScalaSyncPluginTest.java b/scala/tests/unittests/com/google/idea/blaze/scala/sync/BlazeScalaSyncPluginTest.java
new file mode 100644
index 0000000..6002f5d
--- /dev/null
+++ b/scala/tests/unittests/com/google/idea/blaze/scala/sync/BlazeScalaSyncPluginTest.java
@@ -0,0 +1,83 @@
+/*
+ * Copyright 2017 The Bazel Authors. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.idea.blaze.scala.sync;
+
+import static com.google.common.truth.Truth.assertThat;
+
+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.google.idea.blaze.java.sync.BlazeJavaSyncPlugin;
+import com.intellij.openapi.extensions.impl.ExtensionPointImpl;
+import org.jetbrains.annotations.NotNull;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** Unit tests for {@link BlazeScalaSyncPlugin} */
+@RunWith(JUnit4.class)
+public class BlazeScalaSyncPluginTest extends BlazeTestCase {
+  private final ErrorCollector errorCollector = new ErrorCollector();
+  private BlazeContext context;
+
+  @Override
+  protected void initTest(
+      @NotNull Container applicationServices, @NotNull Container projectServices) {
+    super.initTest(applicationServices, projectServices);
+    ExtensionPointImpl<BlazeSyncPlugin> syncPlugins =
+        registerExtensionPoint(BlazeSyncPlugin.EP_NAME, BlazeSyncPlugin.class);
+    syncPlugins.registerExtension(new BlazeJavaSyncPlugin());
+    syncPlugins.registerExtension(new BlazeScalaSyncPlugin());
+    context = new BlazeContext();
+    context.addOutputSink(IssueOutput.class, errorCollector);
+  }
+
+  @Test
+  public void testScalaValidAdditionalLanguage() {
+    ProjectViewSet projectViewSet =
+        ProjectViewSet.builder()
+            .add(
+                ProjectView.builder()
+                    .add(ScalarSection.builder(WorkspaceTypeSection.KEY).set(WorkspaceType.JAVA))
+                    .add(
+                        ListSection.builder(AdditionalLanguagesSection.KEY)
+                            .add(LanguageClass.SCALA))
+                    .build())
+            .build();
+    WorkspaceLanguageSettings workspaceLanguageSettings =
+        LanguageSupport.createWorkspaceLanguageSettings(projectViewSet);
+    LanguageSupport.validateLanguageSettings(context, workspaceLanguageSettings);
+    errorCollector.assertNoIssues();
+    assertThat(workspaceLanguageSettings)
+        .isEqualTo(
+            new WorkspaceLanguageSettings(
+                WorkspaceType.JAVA,
+                ImmutableSet.of(LanguageClass.GENERIC, LanguageClass.JAVA, LanguageClass.SCALA)));
+  }
+}
diff --git a/scala/tests/unittests/com/google/idea/blaze/scala/sync/importer/BlazeScalaWorkspaceImporterTest.java b/scala/tests/unittests/com/google/idea/blaze/scala/sync/importer/BlazeScalaWorkspaceImporterTest.java
new file mode 100644
index 0000000..363bcfb
--- /dev/null
+++ b/scala/tests/unittests/com/google/idea/blaze/scala/sync/importer/BlazeScalaWorkspaceImporterTest.java
@@ -0,0 +1,573 @@
+/*
+ * Copyright 2017 The Bazel Authors. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.idea.blaze.scala.sync.importer;
+
+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.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.TargetMap;
+import com.google.idea.blaze.base.ideinfo.TargetMapBuilder;
+import com.google.idea.blaze.base.model.LibraryKey;
+import com.google.idea.blaze.base.model.primitives.LanguageClass;
+import com.google.idea.blaze.base.model.primitives.WorkspacePath;
+import com.google.idea.blaze.base.model.primitives.WorkspaceRoot;
+import com.google.idea.blaze.base.model.primitives.WorkspaceType;
+import com.google.idea.blaze.base.prefetch.MockPrefetchService;
+import com.google.idea.blaze.base.prefetch.PrefetchService;
+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.sections.DirectoryEntry;
+import com.google.idea.blaze.base.projectview.section.sections.DirectorySection;
+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.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.sync.projectview.WorkspaceLanguageSettings;
+import com.google.idea.blaze.base.sync.workspace.ArtifactLocationDecoder;
+import com.google.idea.blaze.java.sync.BlazeJavaSyncAugmenter;
+import com.google.idea.blaze.java.sync.importer.BlazeJavaWorkspaceImporter;
+import com.google.idea.blaze.java.sync.importer.JavaSourceFilter;
+import com.google.idea.blaze.java.sync.jdeps.JdepsMap;
+import com.google.idea.blaze.java.sync.model.BlazeContentEntry;
+import com.google.idea.blaze.java.sync.model.BlazeJarLibrary;
+import com.google.idea.blaze.java.sync.model.BlazeJavaImportResult;
+import com.google.idea.blaze.java.sync.model.BlazeSourceDirectory;
+import com.google.idea.blaze.java.sync.source.JavaLikeLanguage;
+import com.google.idea.blaze.java.sync.source.JavaSourcePackageReader;
+import com.google.idea.blaze.java.sync.source.PackageManifestReader;
+import com.google.idea.blaze.java.sync.source.SourceArtifact;
+import com.google.idea.blaze.scala.sync.model.BlazeScalaImportResult;
+import com.google.idea.blaze.scala.sync.source.ScalaJavaLikeLanguage;
+import com.intellij.openapi.extensions.ExtensionPoint;
+import java.io.File;
+import java.util.Map;
+import javax.annotation.Nullable;
+import org.jetbrains.annotations.NotNull;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** Tests for {@link BlazeScalaWorkspaceImporter} */
+@RunWith(JUnit4.class)
+public class BlazeScalaWorkspaceImporterTest extends BlazeTestCase {
+  private final WorkspaceRoot workspaceRoot = new WorkspaceRoot(new File("/root"));
+  private BlazeContext context;
+  private final ErrorCollector errorCollector = new ErrorCollector();
+
+  @Override
+  @SuppressWarnings("FunctionalInterfaceClash") // False positive on getDeclaredPackageOfJavaFile.
+  protected void initTest(
+      @NotNull Container applicationServices, @NotNull Container projectServices) {
+    super.initTest(applicationServices, projectServices);
+    context = new BlazeContext();
+    context.addOutputSink(IssueOutput.class, errorCollector);
+
+    registerExtensionPoint(BlazeJavaSyncAugmenter.EP_NAME, BlazeJavaSyncAugmenter.class);
+
+    BlazeImportSettingsManager importSettingsManager = new BlazeImportSettingsManager();
+    importSettingsManager.setImportSettings(
+        new BlazeImportSettings("", "", "", "", BuildSystem.Blaze));
+    projectServices.register(BlazeImportSettingsManager.class, importSettingsManager);
+
+    applicationServices.register(PrefetchService.class, new MockPrefetchService());
+    applicationServices.register(PackageManifestReader.class, new PackageManifestReader());
+
+    // will silently fall back to FilePathJavaPackageReader
+    applicationServices.register(
+        JavaSourcePackageReader.class,
+        new JavaSourcePackageReader() {
+          @Nullable
+          @Override
+          public String getDeclaredPackageOfJavaFile(
+              BlazeContext context,
+              ArtifactLocationDecoder artifactLocationDecoder,
+              SourceArtifact sourceArtifact) {
+            return null;
+          }
+        });
+
+    ExtensionPoint<JavaLikeLanguage> javaLikeLanguages =
+        registerExtensionPoint(JavaLikeLanguage.EP_NAME, JavaLikeLanguage.class);
+    javaLikeLanguages.registerExtension(new JavaLikeLanguage.Java());
+    javaLikeLanguages.registerExtension(new ScalaJavaLikeLanguage());
+  }
+
+  @Test
+  public void testEmptyProject() {
+    ProjectView projectView = ProjectView.builder().build();
+    TargetMap targetMap = TargetMapBuilder.builder().build();
+
+    BlazeJavaImportResult javaImportResult = importJava(projectView, targetMap);
+    BlazeScalaImportResult scalaImportResult = importScala(projectView, targetMap);
+    errorCollector.assertNoIssues();
+
+    assertThat(javaImportResult.libraries).isEmpty();
+    assertThat(javaImportResult.contentEntries).isEmpty();
+    assertThat(javaImportResult.javaSourceFiles).isEmpty();
+    assertThat(javaImportResult.libraries).isEmpty();
+    assertThat(scalaImportResult.libraries).isEmpty();
+  }
+
+  @Test
+  public void testSingleScalaBinary() {
+    ProjectView projectView =
+        ProjectView.builder()
+            .add(
+                ListSection.builder(DirectorySection.KEY)
+                    .add(DirectoryEntry.include(new WorkspacePath("src/main/scala/apps/example"))))
+            .build();
+
+    TargetMap targetMap =
+        TargetMapBuilder.builder()
+            .addTarget(
+                TargetIdeInfo.builder()
+                    .setLabel("//src/main/scala/apps/example:example")
+                    .setBuildFile(source("src/main/scala/apps/example/BUILD"))
+                    .setKind("scala_binary")
+                    .addSource(source("src/main/scala/apps/example/Main.scala"))
+                    .addSource(source("src/main/scala/apps/example/subdir/SubdirHelper.scala"))
+                    .setJavaInfo(
+                        JavaIdeInfo.builder()
+                            .addJar(
+                                LibraryArtifact.builder()
+                                    .setInterfaceJar(gen("src/main/scala/apps/example/example.jar"))
+                                    .setClassJar(gen("src/main/scala/apps/example/example.jar")))))
+            .build();
+
+    BlazeJavaImportResult javaImportResult = importJava(projectView, targetMap);
+    BlazeScalaImportResult scalaImportResult = importScala(projectView, targetMap);
+    errorCollector.assertNoIssues();
+
+    assertThat(javaImportResult.contentEntries)
+        .containsExactly(
+            BlazeContentEntry.builder("/root/src/main/scala/apps/example")
+                .addSource(
+                    BlazeSourceDirectory.builder("/root/src/main/scala/apps/example")
+                        .setPackagePrefix("apps.example")
+                        .build())
+                .build());
+    assertThat(javaImportResult.libraries).isEmpty();
+    assertThat(javaImportResult.javaSourceFiles)
+        .containsExactly(
+            source("src/main/scala/apps/example/Main.scala"),
+            source("src/main/scala/apps/example/subdir/SubdirHelper.scala"));
+    assertThat(scalaImportResult.libraries).isEmpty();
+  }
+
+  @Test
+  public void testScalaBinaryWithMultipleLibraries() {
+    ProjectView projectView =
+        ProjectView.builder()
+            .add(
+                ListSection.builder(DirectorySection.KEY)
+                    .add(DirectoryEntry.include(new WorkspacePath("src/main/scala/apps/example"))))
+            .build();
+
+    TargetMap targetMap =
+        TargetMapBuilder.builder()
+            .addTarget(
+                TargetIdeInfo.builder()
+                    .setLabel("//src/main/scala/apps/example:example")
+                    .setBuildFile(source("src/main/scala/apps/example/BUILD"))
+                    .setKind("scala_binary")
+                    .addSource(source("src/main/scala/apps/example/Main.scala"))
+                    .addSource(source("src/main/scala/apps/example/subdir/SubdirHelper.scala"))
+                    .setJavaInfo(
+                        JavaIdeInfo.builder()
+                            .addJar(
+                                LibraryArtifact.builder()
+                                    .setInterfaceJar(gen("src/main/scala/apps/example/example.jar"))
+                                    .setClassJar(gen("src/main/scala/apps/example/example.jar"))))
+                    .addDependency("//src/main/scala/some/library1:library1")
+                    .addDependency("//src/main/java/other/library2:library2"))
+            .addTarget(
+                TargetIdeInfo.builder()
+                    .setLabel("//src/main/scala/some/library1:library1")
+                    .setBuildFile(source("src/main/scala/some/library1/BUILD"))
+                    .setKind("scala_library")
+                    .addSource(source("src/main/scala/some/library1/Library.scala"))
+                    .setJavaInfo(
+                        JavaIdeInfo.builder()
+                            .addJar(
+                                LibraryArtifact.builder()
+                                    .setInterfaceJar(
+                                        gen("src/main/scala/some/library1/library1_ijar.jar"))
+                                    .setClassJar(gen("src/main/scala/some/library1/library1.jar"))))
+                    .addDependency("//src/main/java/other/import:import"))
+            .addTarget(
+                TargetIdeInfo.builder()
+                    .setLabel("//src/main/java/other/library2:library2")
+                    .setBuildFile(source("src/main/java/other/library2/BUILD"))
+                    .setKind("java_library")
+                    .addSource(source("src/main/java/other/library2/Library.java"))
+                    .setJavaInfo(
+                        JavaIdeInfo.builder()
+                            .addJar(
+                                LibraryArtifact.builder()
+                                    .setInterfaceJar(
+                                        gen("src/main/java/other/library2/liblibrary2-ijar.jar"))
+                                    .setClassJar(
+                                        gen("src/main/java/other/library2/liblibrary2.jar")))))
+            .addTarget(
+                TargetIdeInfo.builder()
+                    .setLabel("//src/main/java/other/import:import")
+                    .setBuildFile(source("src/main/java/other/import/BUILD"))
+                    .setKind("java_import")
+                    .setJavaInfo(
+                        JavaIdeInfo.builder()
+                            .addJar(
+                                LibraryArtifact.builder()
+                                    .setInterfaceJar(
+                                        gen("src/main/java/other/import/libimport-ijar.jar"))
+                                    .setClassJar(gen("src/main/java/other/import/libimport.jar")))))
+            .build();
+
+    BlazeJavaImportResult javaImportResult = importJava(projectView, targetMap);
+    BlazeScalaImportResult scalaImportResult = importScala(projectView, targetMap);
+    errorCollector.assertNoIssues();
+
+    assertThat(javaImportResult.contentEntries)
+        .containsExactly(
+            BlazeContentEntry.builder("/root/src/main/scala/apps/example")
+                .addSource(
+                    BlazeSourceDirectory.builder("/root/src/main/scala/apps/example")
+                        .setPackagePrefix("apps.example")
+                        .build())
+                .build());
+    // Direct library deps will be double counted.
+    assertThat(javaImportResult.libraries).hasSize(2);
+    assertThat(hasLibrary(javaImportResult.libraries, "library1_ijar")).isTrue();
+    assertThat(hasLibrary(javaImportResult.libraries, "library2-ijar")).isTrue();
+    assertThat(javaImportResult.javaSourceFiles)
+        .containsExactly(
+            source("src/main/scala/apps/example/Main.scala"),
+            source("src/main/scala/apps/example/subdir/SubdirHelper.scala"));
+    assertThat(scalaImportResult.libraries).hasSize(3);
+    assertThat(hasLibrary(scalaImportResult.libraries, "library1_ijar")).isTrue();
+    assertThat(hasLibrary(scalaImportResult.libraries, "library2-ijar")).isTrue();
+    assertThat(hasLibrary(scalaImportResult.libraries, "import-ijar")).isTrue();
+  }
+
+  @Test
+  public void testScalaAndJavaBinary() {
+    ProjectView projectView =
+        ProjectView.builder()
+            .add(
+                ListSection.builder(DirectorySection.KEY)
+                    .add(DirectoryEntry.include(new WorkspacePath("src/main/scala/apps/example")))
+                    .add(DirectoryEntry.include(new WorkspacePath("src/main/java/apps/example"))))
+            .build();
+
+    TargetMap targetMap =
+        TargetMapBuilder.builder()
+            .addTarget(
+                TargetIdeInfo.builder()
+                    .setLabel("//src/main/scala/apps/example:example")
+                    .setBuildFile(source("src/main/scala/apps/example/BUILD"))
+                    .setKind("scala_binary")
+                    .addSource(source("src/main/scala/apps/example/Main.scala"))
+                    .addSource(source("src/main/scala/apps/example/subdir/SubdirHelper.scala"))
+                    .setJavaInfo(
+                        JavaIdeInfo.builder()
+                            .addJar(
+                                LibraryArtifact.builder()
+                                    .setInterfaceJar(gen("src/main/scala/apps/example/example.jar"))
+                                    .setClassJar(gen("src/main/scala/apps/example/example.jar")))))
+            .addTarget(
+                TargetIdeInfo.builder()
+                    .setLabel("//src/main/java/apps/example:example")
+                    .setBuildFile(source("src/main/java/apps/example/BUILD"))
+                    .setKind("java_binary")
+                    .addSource(source("src/main/java/apps/example/Main.java"))
+                    .addSource(source("src/main/java/apps/example/subdir/SubdirHelper.java"))
+                    .setJavaInfo(
+                        JavaIdeInfo.builder()
+                            .addJar(
+                                LibraryArtifact.builder()
+                                    .setInterfaceJar(gen("src/main/java/apps/example/example.jar"))
+                                    .setClassJar(gen("src/main/java/apps/example/example.jar")))))
+            .build();
+
+    BlazeJavaImportResult javaImportResult = importJava(projectView, targetMap);
+    BlazeScalaImportResult scalaImportResult = importScala(projectView, targetMap);
+    errorCollector.assertNoIssues();
+
+    assertThat(javaImportResult.contentEntries)
+        .containsExactly(
+            BlazeContentEntry.builder("/root/src/main/scala/apps/example")
+                .addSource(
+                    BlazeSourceDirectory.builder("/root/src/main/scala/apps/example")
+                        .setPackagePrefix("apps.example")
+                        .build())
+                .build(),
+            BlazeContentEntry.builder("/root/src/main/java/apps/example")
+                .addSource(
+                    BlazeSourceDirectory.builder("/root/src/main/java/apps/example")
+                        .setPackagePrefix("apps.example")
+                        .build())
+                .build());
+    assertThat(javaImportResult.libraries).isEmpty();
+    assertThat(javaImportResult.javaSourceFiles)
+        .containsExactly(
+            source("src/main/scala/apps/example/Main.scala"),
+            source("src/main/scala/apps/example/subdir/SubdirHelper.scala"),
+            source("src/main/java/apps/example/Main.java"),
+            source("src/main/java/apps/example/subdir/SubdirHelper.java"));
+    assertThat(scalaImportResult.libraries).isEmpty();
+  }
+
+  @Test
+  public void testTwoScalaBinariesWithSharedLibrary() {
+    ProjectView projectView =
+        ProjectView.builder()
+            .add(
+                ListSection.builder(DirectorySection.KEY)
+                    .add(DirectoryEntry.include(new WorkspacePath("src/main/scala/apps/example")))
+                    .add(DirectoryEntry.include(new WorkspacePath("src/main/scala/apps/other"))))
+            .build();
+
+    TargetMap targetMap =
+        TargetMapBuilder.builder()
+            .addTarget(
+                TargetIdeInfo.builder()
+                    .setLabel("//src/main/scala/apps/example:example")
+                    .setBuildFile(source("src/main/scala/apps/example/BUILD"))
+                    .setKind("scala_binary")
+                    .addSource(source("src/main/scala/apps/example/Main.scala"))
+                    .addSource(source("src/main/scala/apps/example/subdir/SubdirHelper.scala"))
+                    .setJavaInfo(
+                        JavaIdeInfo.builder()
+                            .addJar(
+                                LibraryArtifact.builder()
+                                    .setInterfaceJar(gen("src/main/scala/apps/example/example.jar"))
+                                    .setClassJar(gen("src/main/scala/apps/example/example.jar"))))
+                    .addDependency("//src/main/scala/some/library:library"))
+            .addTarget(
+                TargetIdeInfo.builder()
+                    .setLabel("//src/main/scala/apps/other:other")
+                    .setBuildFile(source("src/main/scala/apps/other/BUILD"))
+                    .setKind("scala_binary")
+                    .addSource(source("src/main/scala/apps/other/Main.scala"))
+                    .addSource(source("src/main/scala/apps/other/subdir/SubdirHelper.scala"))
+                    .setJavaInfo(
+                        JavaIdeInfo.builder()
+                            .addJar(
+                                LibraryArtifact.builder()
+                                    .setInterfaceJar(gen("src/main/scala/apps/other/other.jar"))
+                                    .setClassJar(gen("src/main/scala/apps/other/other.jar"))))
+                    .addDependency("//src/main/scala/some/library:library"))
+            .addTarget(
+                TargetIdeInfo.builder()
+                    .setLabel("//src/main/scala/some/library:library")
+                    .setBuildFile(source("src/main/scala/some/library/BUILD"))
+                    .setKind("scala_library")
+                    .addSource(source("src/main/scala/some/library/Library.scala"))
+                    .setJavaInfo(
+                        JavaIdeInfo.builder()
+                            .addJar(
+                                LibraryArtifact.builder()
+                                    .setInterfaceJar(
+                                        gen("src/main/scala/some/library/library_ijar.jar"))
+                                    .setClassJar(gen("src/main/scala/some/library/library.jar")))))
+            .build();
+
+    BlazeJavaImportResult javaImportResult = importJava(projectView, targetMap);
+    BlazeScalaImportResult scalaImportResult = importScala(projectView, targetMap);
+    errorCollector.assertNoIssues();
+
+    assertThat(javaImportResult.contentEntries)
+        .containsExactly(
+            BlazeContentEntry.builder("/root/src/main/scala/apps/example")
+                .addSource(
+                    BlazeSourceDirectory.builder("/root/src/main/scala/apps/example")
+                        .setPackagePrefix("apps.example")
+                        .build())
+                .build(),
+            BlazeContentEntry.builder("/root/src/main/scala/apps/other")
+                .addSource(
+                    BlazeSourceDirectory.builder("/root/src/main/scala/apps/other")
+                        .setPackagePrefix("apps.other")
+                        .build())
+                .build());
+    // Direct library deps will be double counted.
+    assertThat(javaImportResult.libraries).hasSize(1);
+    assertThat(hasLibrary(javaImportResult.libraries, "library_ijar")).isTrue();
+    assertThat(javaImportResult.javaSourceFiles)
+        .containsExactly(
+            source("src/main/scala/apps/example/Main.scala"),
+            source("src/main/scala/apps/example/subdir/SubdirHelper.scala"),
+            source("src/main/scala/apps/other/Main.scala"),
+            source("src/main/scala/apps/other/subdir/SubdirHelper.scala"));
+    assertThat(scalaImportResult.libraries).hasSize(1);
+    assertThat(hasLibrary(scalaImportResult.libraries, "library_ijar")).isTrue();
+  }
+
+  @Test
+  public void testSourceRulesNotInLibraries() {
+    ProjectView projectView =
+        ProjectView.builder()
+            .add(
+                ListSection.builder(DirectorySection.KEY)
+                    .add(DirectoryEntry.include(new WorkspacePath("src/main/scala/apps/example")))
+                    .add(DirectoryEntry.include(new WorkspacePath("src/main/scala/some/library1")))
+                    .add(DirectoryEntry.include(new WorkspacePath("src/main/java/other/library2"))))
+            .build();
+
+    TargetMap targetMap =
+        TargetMapBuilder.builder()
+            .addTarget(
+                TargetIdeInfo.builder()
+                    .setLabel("//src/main/scala/apps/example:example")
+                    .setBuildFile(source("src/main/scala/apps/example/BUILD"))
+                    .setKind("scala_binary")
+                    .addSource(source("src/main/scala/apps/example/Main.scala"))
+                    .addSource(source("src/main/scala/apps/example/subdir/SubdirHelper.scala"))
+                    .setJavaInfo(
+                        JavaIdeInfo.builder()
+                            .addJar(
+                                LibraryArtifact.builder()
+                                    .setInterfaceJar(gen("src/main/scala/apps/example/example.jar"))
+                                    .setClassJar(gen("src/main/scala/apps/example/example.jar"))))
+                    .addDependency("//src/main/scala/some/library1:library1")
+                    .addDependency("//src/main/java/other/library2:library2"))
+            .addTarget(
+                TargetIdeInfo.builder()
+                    .setLabel("//src/main/scala/some/library1:library1")
+                    .setBuildFile(source("src/main/scala/some/library1/BUILD"))
+                    .setKind("scala_library")
+                    .addSource(source("src/main/scala/some/library1/Library.scala"))
+                    .setJavaInfo(
+                        JavaIdeInfo.builder()
+                            .addJar(
+                                LibraryArtifact.builder()
+                                    .setInterfaceJar(
+                                        gen("src/main/scala/some/library1/library1_ijar.jar"))
+                                    .setClassJar(
+                                        gen("src/main/scala/some/library1/library1.jar")))))
+            .addTarget(
+                TargetIdeInfo.builder()
+                    .setLabel("//src/main/java/other/library2:library2")
+                    .setBuildFile(source("src/main/java/other/library2/BUILD"))
+                    .setKind("java_library")
+                    .addSource(source("src/main/java/other/library2/Library.java"))
+                    .setJavaInfo(
+                        JavaIdeInfo.builder()
+                            .addJar(
+                                LibraryArtifact.builder()
+                                    .setInterfaceJar(
+                                        gen("src/main/java/other/library2/libibrary2-ijar.jar"))
+                                    .setClassJar(
+                                        gen("src/main/java/other/library2/liblibrary2.jar")))))
+            .build();
+
+    BlazeJavaImportResult javaImportResult = importJava(projectView, targetMap);
+    BlazeScalaImportResult scalaImportResult = importScala(projectView, targetMap);
+    errorCollector.assertNoIssues();
+
+    assertThat(javaImportResult.contentEntries)
+        .containsExactly(
+            BlazeContentEntry.builder("/root/src/main/scala/apps/example")
+                .addSource(
+                    BlazeSourceDirectory.builder("/root/src/main/scala/apps/example")
+                        .setPackagePrefix("apps.example")
+                        .build())
+                .build(),
+            BlazeContentEntry.builder("/root/src/main/scala/some/library1")
+                .addSource(
+                    BlazeSourceDirectory.builder("/root/src/main/scala/some/library1")
+                        .setPackagePrefix("some.library1")
+                        .build())
+                .build(),
+            BlazeContentEntry.builder("/root/src/main/java/other/library2")
+                .addSource(
+                    BlazeSourceDirectory.builder("/root/src/main/java/other/library2")
+                        .setPackagePrefix("other.library2")
+                        .build())
+                .build());
+    assertThat(javaImportResult.libraries).isEmpty();
+    assertThat(javaImportResult.javaSourceFiles)
+        .containsExactly(
+            source("src/main/scala/apps/example/Main.scala"),
+            source("src/main/scala/apps/example/subdir/SubdirHelper.scala"),
+            source("src/main/scala/some/library1/Library.scala"),
+            source("src/main/java/other/library2/Library.java"));
+    assertThat(scalaImportResult.libraries).isEmpty();
+  }
+
+  private static boolean hasLibrary(
+      Map<LibraryKey, BlazeJarLibrary> libraries, String libraryName) {
+    return libraries
+        .values()
+        .stream()
+        .anyMatch(
+            library ->
+                library
+                    .libraryArtifact
+                    .jarForIntellijLibrary()
+                    .relativePath
+                    .endsWith(libraryName + ".jar"));
+  }
+
+  private BlazeJavaImportResult importJava(ProjectView projectView, TargetMap targetMap) {
+    ProjectViewSet projectViewSet = ProjectViewSet.builder().add(projectView).build();
+    WorkspaceLanguageSettings languageSettings =
+        new WorkspaceLanguageSettings(
+            WorkspaceType.JAVA,
+            ImmutableSet.of(LanguageClass.GENERIC, LanguageClass.SCALA, LanguageClass.JAVA));
+    JavaSourceFilter sourceFilter =
+        new JavaSourceFilter(project, workspaceRoot, projectViewSet, targetMap);
+    JdepsMap jdepsMap = key -> ImmutableList.of();
+    ArtifactLocationDecoder decoder = location -> new File(location.getRelativePath());
+    return new BlazeJavaWorkspaceImporter(
+            project,
+            workspaceRoot,
+            projectViewSet,
+            languageSettings,
+            targetMap,
+            sourceFilter,
+            jdepsMap,
+            null,
+            decoder)
+        .importWorkspace(context);
+  }
+
+  private BlazeScalaImportResult importScala(ProjectView projectView, TargetMap targetMap) {
+    ProjectViewSet projectViewSet = ProjectViewSet.builder().add(projectView).build();
+    return new BlazeScalaWorkspaceImporter(project, workspaceRoot, projectViewSet, targetMap)
+        .importWorkspace();
+  }
+
+  private static ArtifactLocation source(String relativePath) {
+    return ArtifactLocation.builder().setRelativePath(relativePath).setIsSource(true).build();
+  }
+
+  private static ArtifactLocation gen(String relativePath) {
+    return ArtifactLocation.builder()
+        .setRootExecutionPathFragment("blaze-out/bin")
+        .setRelativePath(relativePath)
+        .setIsSource(false)
+        .build();
+  }
+}
diff --git a/sdkcompat/BUILD b/sdkcompat/BUILD
index ce19ce5..a152a21 100644
--- a/sdkcompat/BUILD
+++ b/sdkcompat/BUILD
@@ -6,20 +6,14 @@
 
 java_library(
     name = "sdkcompat",
-    srcs = select_for_plugin_api({
-        "android-studio-145.1617.8": glob(["v145/**"]),
-        "android-studio-2.3.0.3": glob(["v162/**"]),
-        "android-studio-2.3.0.4": glob(["v162/**"]),
-        "intellij-2016.3.1": glob(["v163/**"]),
-        "intellij-162.2032.8": glob(["v162/**"]),
-        "clion-162.1967.7": glob(
-            ["v162/**"],
-            exclude = ["v162/com/google/idea/sdkcompat/debugger/**"],
-        ),
-    }),
     visibility = ["//visibility:public"],
-    deps = [
-        "//intellij_platform_sdk:plugin_api",
-        "@jsr305_annotations//jar",
-    ],
+    exports = select_for_plugin_api({
+        "android-studio-2.3.0.8": ["//sdkcompat/v162"],
+        "android-studio-2.3.1.0": ["//sdkcompat/v162"],
+        "intellij-2017.1.1": ["//sdkcompat/v171"],
+        "intellij-2016.3.1": ["//sdkcompat/v163"],
+        "clion-162.1967.7": ["//sdkcompat/v162"],
+        "clion-2016.3.2": ["//sdkcompat/v163"],
+        "clion-2017.1.1": ["//sdkcompat/v171"],
+    }),
 )
diff --git a/sdkcompat/v145/com/google/idea/sdkcompat/codestyle/CodeStyleManagerSdkCompatAdapter.java b/sdkcompat/v145/com/google/idea/sdkcompat/codestyle/CodeStyleManagerSdkCompatAdapter.java
deleted file mode 100644
index 6051cc9..0000000
--- a/sdkcompat/v145/com/google/idea/sdkcompat/codestyle/CodeStyleManagerSdkCompatAdapter.java
+++ /dev/null
@@ -1,6 +0,0 @@
-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
deleted file mode 100644
index 6dfc995..0000000
--- a/sdkcompat/v145/com/google/idea/sdkcompat/debugger/GenericDebuggerRunnerSdkCompatAdapter.java
+++ /dev/null
@@ -1,25 +0,0 @@
-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/transactions/Transactions.java b/sdkcompat/v145/com/google/idea/sdkcompat/transactions/Transactions.java
deleted file mode 100644
index 94ab905..0000000
--- a/sdkcompat/v145/com/google/idea/sdkcompat/transactions/Transactions.java
+++ /dev/null
@@ -1,16 +0,0 @@
-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
deleted file mode 100644
index df055ea..0000000
--- a/sdkcompat/v145/com/google/idea/sdkcompat/vcs/ChangeListManagerSdkCompatAdapter.java
+++ /dev/null
@@ -1,12 +0,0 @@
-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/BUILD b/sdkcompat/v162/BUILD
new file mode 100644
index 0000000..8eb53e7
--- /dev/null
+++ b/sdkcompat/v162/BUILD
@@ -0,0 +1,34 @@
+# Description: Indirections for SDK changes to the underlying platform library.
+
+licenses(["notice"])  # Apache 2.0
+
+load("//intellij_platform_sdk:build_defs.bzl", "select_for_ide")
+
+java_library(
+    name = "v162",
+    srcs = glob([
+        "com/google/idea/sdkcompat/codestyle/**",
+        "com/google/idea/sdkcompat/smrunner/**",
+        "com/google/idea/sdkcompat/transactions/**",
+        "com/google/idea/sdkcompat/vcs/**",
+    ]) + select_for_ide(
+        android_studio = glob([
+            "com/google/idea/sdkcompat/cidr/**",
+        ]),
+        clion = glob([
+            "com/google/idea/sdkcompat/cidr/**",
+            "com/google/idea/sdkcompat/clion/**",
+        ]),
+        intellij = glob([
+            "com/google/idea/sdkcompat/python/**",
+            "com/google/idea/sdkcompat/dart/**",
+        ]),
+    ),
+    visibility = ["//sdkcompat:__pkg__"],
+    deps = [
+        "//intellij_platform_sdk:plugin_api",
+        "@jsr305_annotations//jar",
+    ] + select_for_ide(
+        intellij = ["//third_party/python"],
+    ),
+)
diff --git a/sdkcompat/v162/com/google/idea/sdkcompat/cidr/CidrCompilerSwitchesAdapter.java b/sdkcompat/v162/com/google/idea/sdkcompat/cidr/CidrCompilerSwitchesAdapter.java
new file mode 100644
index 0000000..d60a1a6
--- /dev/null
+++ b/sdkcompat/v162/com/google/idea/sdkcompat/cidr/CidrCompilerSwitchesAdapter.java
@@ -0,0 +1,20 @@
+package com.google.idea.sdkcompat.cidr;
+
+import com.jetbrains.cidr.lang.toolchains.CidrCompilerSwitches;
+import java.util.List;
+
+/** Adapter to bridge different SDK versions. */
+public class CidrCompilerSwitchesAdapter {
+  /** Old interface does not know anything about CidrCompilerSwitches.Format */
+  public static List<String> getFileArgs(CidrCompilerSwitches switches) {
+    return switches.getFileArgs();
+  }
+
+  public static List<String> getCommandLineArgs(CidrCompilerSwitches switches) {
+    return switches.getCommandLineArgs();
+  }
+
+  public static String getCommandLineString(CidrCompilerSwitches switches) {
+    return switches.getCommandLineString();
+  }
+}
diff --git a/sdkcompat/v162/com/google/idea/sdkcompat/cidr/CidrConsoleBuilderAdapter.java b/sdkcompat/v162/com/google/idea/sdkcompat/cidr/CidrConsoleBuilderAdapter.java
new file mode 100644
index 0000000..63eabeb
--- /dev/null
+++ b/sdkcompat/v162/com/google/idea/sdkcompat/cidr/CidrConsoleBuilderAdapter.java
@@ -0,0 +1,12 @@
+package com.google.idea.sdkcompat.cidr;
+
+import com.intellij.openapi.project.Project;
+import com.jetbrains.cidr.execution.CidrConsoleBuilder;
+
+/** Adapter to bridge different SDK versions. */
+public class CidrConsoleBuilderAdapter extends CidrConsoleBuilder {
+
+  public CidrConsoleBuilderAdapter(Project project) {
+    super(project);
+  }
+}
diff --git a/sdkcompat/v162/com/google/idea/sdkcompat/cidr/CidrSwitchBuilderAdapter.java b/sdkcompat/v162/com/google/idea/sdkcompat/cidr/CidrSwitchBuilderAdapter.java
new file mode 100644
index 0000000..c39c08b
--- /dev/null
+++ b/sdkcompat/v162/com/google/idea/sdkcompat/cidr/CidrSwitchBuilderAdapter.java
@@ -0,0 +1,24 @@
+package com.google.idea.sdkcompat.cidr;
+
+import com.google.common.base.Joiner;
+import com.jetbrains.cidr.lang.toolchains.CidrSwitchBuilder;
+import java.util.List;
+import java.util.stream.Collectors;
+
+/** Adapter to bridge different SDK versions. */
+public class CidrSwitchBuilderAdapter extends CidrSwitchBuilder {
+  /**
+   * Old CidrSwitchBuilder is unable to deal with options with spaces embedded. This is a hack to
+   * preserve the old behaviour for 2016.2 Original hack explanation: - this list of switches is
+   * currently only used in one place -- GCCCompiler.tryRunGCC. - list is written to an argument
+   * file, whitespace-separated, then passed as a @file arg to clang. In this context, escaped
+   * whitespace within a single arg is not handled. Currently, the only way (short of using
+   * reflection) to ensure unescaped whitespace is to have CidrSwitchBuilder treat whitespace as a
+   * delimiter between args.
+   */
+  public CidrSwitchBuilderAdapter addAllRaw(List<String> switches) {
+    switches = switches.stream().map(flag -> flag.replace("\\ ", " ")).collect(Collectors.toList());
+    addAll(Joiner.on(" ").join(switches), CidrSwitchBuilder.Format.FILE_ARGS);
+    return this;
+  }
+}
diff --git a/sdkcompat/v162/com/google/idea/sdkcompat/cidr/OCResolveConfigurationAdapter.java b/sdkcompat/v162/com/google/idea/sdkcompat/cidr/OCResolveConfigurationAdapter.java
new file mode 100644
index 0000000..912eb77
--- /dev/null
+++ b/sdkcompat/v162/com/google/idea/sdkcompat/cidr/OCResolveConfigurationAdapter.java
@@ -0,0 +1,20 @@
+package com.google.idea.sdkcompat.cidr;
+
+import com.intellij.openapi.vfs.VirtualFile;
+import com.jetbrains.cidr.lang.OCLanguageKind;
+import com.jetbrains.cidr.lang.workspace.OCResolveConfiguration;
+import java.util.Collection;
+import java.util.List;
+import java.util.Set;
+
+/** Adapter to bridge different SDK versions. */
+public interface OCResolveConfigurationAdapter extends OCResolveConfiguration {
+  /* v171 */
+  public List<VirtualFile> getPrecompiledHeaders(OCLanguageKind kind, VirtualFile sourceFile);
+
+  /* v171 */
+  public Collection<VirtualFile> getSources();
+
+  /* v171 */
+  public Set<VirtualFile> getPrecompiledHeaders();
+}
diff --git a/sdkcompat/v162/com/google/idea/sdkcompat/cidr/OCWorkspaceAdapter.java b/sdkcompat/v162/com/google/idea/sdkcompat/cidr/OCWorkspaceAdapter.java
new file mode 100644
index 0000000..99c1d36
--- /dev/null
+++ b/sdkcompat/v162/com/google/idea/sdkcompat/cidr/OCWorkspaceAdapter.java
@@ -0,0 +1,28 @@
+package com.google.idea.sdkcompat.cidr;
+
+import com.intellij.openapi.project.Project;
+import com.jetbrains.cidr.lang.workspace.OCResolveConfiguration;
+import com.jetbrains.cidr.lang.workspace.OCWorkspace;
+import com.jetbrains.cidr.lang.workspace.OCWorkspaceModificationTrackers;
+import javax.annotation.Nullable;
+
+/** Adapter to bridge different SDK versions. */
+public abstract class OCWorkspaceAdapter implements OCWorkspace {
+
+  private final Project project;
+
+  protected OCWorkspaceAdapter(Project project) {
+    this.project = project;
+  }
+
+  @Nullable
+  @Override
+  public OCResolveConfiguration getSelectedResolveConfiguration() {
+    return null;
+  }
+
+  @Override
+  public OCWorkspaceModificationTrackers getModificationTrackers() {
+    return OCWorkspaceModificationTrackersCompatUtils.getTrackers(project);
+  }
+}
diff --git a/sdkcompat/v162/com/google/idea/sdkcompat/cidr/OCWorkspaceModificationTrackersCompatUtils.java b/sdkcompat/v162/com/google/idea/sdkcompat/cidr/OCWorkspaceModificationTrackersCompatUtils.java
new file mode 100644
index 0000000..4b17895
--- /dev/null
+++ b/sdkcompat/v162/com/google/idea/sdkcompat/cidr/OCWorkspaceModificationTrackersCompatUtils.java
@@ -0,0 +1,25 @@
+package com.google.idea.sdkcompat.cidr;
+
+import com.intellij.openapi.project.Project;
+import com.jetbrains.cidr.lang.workspace.OCWorkspaceModificationTrackers;
+import java.util.HashMap;
+import java.util.Map;
+
+/** Handles changes to modification trackers between our supported versions. */
+public class OCWorkspaceModificationTrackersCompatUtils {
+
+  private static final Map<Project, OCWorkspaceModificationTrackers> trackers = new HashMap<>();
+
+  public static OCWorkspaceModificationTrackers getTrackers(Project project) {
+    return trackers.computeIfAbsent(project, OCWorkspaceModificationTrackers::new);
+  }
+
+  /** Must be called inside a write action, on the EDT. */
+  public static void incrementModificationCounts(Project project) {
+    OCWorkspaceModificationTrackers modTrackers = getTrackers(project);
+    modTrackers.getProjectFilesListTracker().incModificationCount();
+    modTrackers.getSourceFilesListTracker().incModificationCount();
+    modTrackers.getBuildConfigurationChangesTracker().incModificationCount();
+    modTrackers.getBuildSettingsChangesTracker().incModificationCount();
+  }
+}
diff --git a/sdkcompat/v162/com/google/idea/sdkcompat/clion/CMakeActionList.java b/sdkcompat/v162/com/google/idea/sdkcompat/clion/CMakeActionList.java
new file mode 100644
index 0000000..9f58cf3
--- /dev/null
+++ b/sdkcompat/v162/com/google/idea/sdkcompat/clion/CMakeActionList.java
@@ -0,0 +1,22 @@
+package com.google.idea.sdkcompat.clion;
+
+import com.google.common.collect.ImmutableSet;
+import com.jetbrains.cidr.cpp.cmake.actions.ChangeCMakeProjectContentRootAction;
+import com.jetbrains.cidr.cpp.cmake.actions.DropCMakeCacheAction;
+import com.jetbrains.cidr.cpp.cmake.actions.OpenCMakeSettingsAction;
+import com.jetbrains.cidr.cpp.cmake.actions.ReloadCMakeProjectAction;
+import com.jetbrains.cidr.cpp.cmake.actions.ToggleCMakeAutoReloadAction;
+
+/** Handles CMake actions which have changed between our supported versions. */
+public class CMakeActionList {
+
+  public static ImmutableSet<String> CMAKE_ACTION_IDS =
+      ImmutableSet.of(
+          ChangeCMakeProjectContentRootAction.ID,
+          DropCMakeCacheAction.ID,
+          OpenCMakeSettingsAction.ID,
+          ReloadCMakeProjectAction.ID,
+          ToggleCMakeAutoReloadAction.ID,
+          // 'CMake' > 'Show Generated CMake Files' action
+          "CMake.ShowGeneratedDir");
+}
diff --git a/sdkcompat/v162/com/google/idea/sdkcompat/codestyle/CodeStyleManagerSdkCompatAdapter.java b/sdkcompat/v162/com/google/idea/sdkcompat/codestyle/CodeStyleManagerSdkCompatAdapter.java
deleted file mode 100644
index 6051cc9..0000000
--- a/sdkcompat/v162/com/google/idea/sdkcompat/codestyle/CodeStyleManagerSdkCompatAdapter.java
+++ /dev/null
@@ -1,6 +0,0 @@
-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/codestyle/DelegatingCodeStyleManagerSdkCompatAdapter.java b/sdkcompat/v162/com/google/idea/sdkcompat/codestyle/DelegatingCodeStyleManagerSdkCompatAdapter.java
new file mode 100644
index 0000000..a16e94a
--- /dev/null
+++ b/sdkcompat/v162/com/google/idea/sdkcompat/codestyle/DelegatingCodeStyleManagerSdkCompatAdapter.java
@@ -0,0 +1,13 @@
+package com.google.idea.sdkcompat.codestyle;
+
+import com.intellij.psi.codeStyle.CodeStyleManager;
+
+/** Adapter to bridge different SDK versions. */
+public abstract class DelegatingCodeStyleManagerSdkCompatAdapter extends CodeStyleManager {
+
+  protected CodeStyleManager delegate;
+
+  protected DelegatingCodeStyleManagerSdkCompatAdapter(CodeStyleManager delegate) {
+    this.delegate = delegate;
+  }
+}
diff --git a/sdkcompat/v162/com/google/idea/sdkcompat/dart/DartSdkCompatUtils.java b/sdkcompat/v162/com/google/idea/sdkcompat/dart/DartSdkCompatUtils.java
new file mode 100644
index 0000000..58f48e4
--- /dev/null
+++ b/sdkcompat/v162/com/google/idea/sdkcompat/dart/DartSdkCompatUtils.java
@@ -0,0 +1,32 @@
+/*
+ * Copyright 2017 The Bazel Authors. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.idea.sdkcompat.dart;
+
+import com.intellij.openapi.project.Project;
+import com.intellij.openapi.roots.impl.libraries.ApplicationLibraryTable;
+import com.intellij.openapi.roots.libraries.Library;
+import javax.annotation.Nullable;
+
+/** Handles changes to the Dart plugin libraries between our supported versions. */
+public class DartSdkCompatUtils {
+
+  public static final String DART_SDK_LIBRARY_NAME = "Dart SDK";
+
+  @Nullable
+  public static Library findDartLibrary(Project project) {
+    return ApplicationLibraryTable.getApplicationTable().getLibraryByName(DART_SDK_LIBRARY_NAME);
+  }
+}
diff --git a/sdkcompat/v162/com/google/idea/sdkcompat/debugger/GenericDebuggerRunnerSdkCompatAdapter.java b/sdkcompat/v162/com/google/idea/sdkcompat/debugger/GenericDebuggerRunnerSdkCompatAdapter.java
deleted file mode 100644
index ed822b3..0000000
--- a/sdkcompat/v162/com/google/idea/sdkcompat/debugger/GenericDebuggerRunnerSdkCompatAdapter.java
+++ /dev/null
@@ -1,6 +0,0 @@
-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/python/PyConfigurationProducersList.java b/sdkcompat/v162/com/google/idea/sdkcompat/python/PyConfigurationProducersList.java
new file mode 100644
index 0000000..b3785da
--- /dev/null
+++ b/sdkcompat/v162/com/google/idea/sdkcompat/python/PyConfigurationProducersList.java
@@ -0,0 +1,17 @@
+package com.google.idea.sdkcompat.python;
+
+import com.google.common.collect.ImmutableList;
+import java.util.Collection;
+
+/** List of python configuration producers for a given plugin version. */
+public class PyConfigurationProducersList {
+
+  public static final Collection<Class<?>> PRODUCERS_TO_SUPPRESS =
+      ImmutableList.of(
+          com.jetbrains.python.run.PythonRunConfigurationProducer.class,
+          com.jetbrains.python.testing.attest.PythonAtTestConfigurationProducer.class,
+          com.jetbrains.python.testing.nosetest.PythonNoseTestConfigurationProducer.class,
+          com.jetbrains.python.testing.doctest.PythonDocTestConfigurationProducer.class,
+          com.jetbrains.python.testing.pytest.PyTestConfigurationProducer.class,
+          com.jetbrains.python.testing.unittest.PythonUnitTestConfigurationProducer.class);
+}
diff --git a/sdkcompat/v162/com/google/idea/sdkcompat/python/PyImportResolverAdapter.java b/sdkcompat/v162/com/google/idea/sdkcompat/python/PyImportResolverAdapter.java
new file mode 100644
index 0000000..9ccba03
--- /dev/null
+++ b/sdkcompat/v162/com/google/idea/sdkcompat/python/PyImportResolverAdapter.java
@@ -0,0 +1,23 @@
+package com.google.idea.sdkcompat.python;
+
+import com.intellij.psi.PsiElement;
+import com.intellij.psi.util.QualifiedName;
+import com.jetbrains.python.psi.impl.PyImportResolver;
+import com.jetbrains.python.psi.resolve.QualifiedNameResolveContext;
+import javax.annotation.Nullable;
+
+/** Adapter to bridge different SDK versions. */
+public interface PyImportResolverAdapter extends PyImportResolver {
+
+  @Nullable
+  PsiElement resolveImportReference(
+      QualifiedName name, PyQualifiedNameResolveContextAdapter context, boolean withRoots);
+
+  @Override
+  @Nullable
+  default PsiElement resolveImportReference(
+      QualifiedName name, QualifiedNameResolveContext context, boolean withRoots) {
+    return resolveImportReference(
+        name, new PyQualifiedNameResolveContextAdapter(context), withRoots);
+  }
+}
diff --git a/sdkcompat/v162/com/google/idea/sdkcompat/python/PyQualifiedNameResolveContextAdapter.java b/sdkcompat/v162/com/google/idea/sdkcompat/python/PyQualifiedNameResolveContextAdapter.java
new file mode 100644
index 0000000..7330e11
--- /dev/null
+++ b/sdkcompat/v162/com/google/idea/sdkcompat/python/PyQualifiedNameResolveContextAdapter.java
@@ -0,0 +1,77 @@
+package com.google.idea.sdkcompat.python;
+
+import com.intellij.openapi.module.Module;
+import com.intellij.openapi.project.Project;
+import com.intellij.openapi.projectRoots.Sdk;
+import com.intellij.psi.PsiElement;
+import com.intellij.psi.PsiFile;
+import com.intellij.psi.PsiManager;
+import com.jetbrains.python.psi.resolve.QualifiedNameResolveContext;
+import javax.annotation.Nullable;
+
+/** Adapter to bridge different SDK versions. */
+public class PyQualifiedNameResolveContextAdapter extends QualifiedNameResolveContext {
+
+  private final QualifiedNameResolveContext delegate;
+
+  PyQualifiedNameResolveContextAdapter(QualifiedNameResolveContext delegate) {
+    this.delegate = delegate;
+  }
+
+  @Override
+  public void copyFrom(QualifiedNameResolveContext context) {
+    delegate.copyFrom(context);
+  }
+
+  @Override
+  public void setFromElement(PsiElement element) {
+    delegate.setFromElement(element);
+  }
+
+  @Override
+  public void setFromModule(Module module) {
+    delegate.setFromModule(module);
+  }
+
+  @Override
+  public void setFromSdk(Project project, Sdk sdk) {
+    delegate.setFromSdk(project, sdk);
+  }
+
+  @Override
+  public void setSdk(Sdk sdk) {
+    delegate.setSdk(sdk);
+  }
+
+  @Override
+  @Nullable
+  public Module getModule() {
+    return delegate.getModule();
+  }
+
+  @Override
+  public boolean isValid() {
+    return delegate.isValid();
+  }
+
+  @Override
+  @Nullable
+  public PsiFile getFootholdFile() {
+    return delegate.getFootholdFile();
+  }
+
+  @Override
+  public PsiManager getPsiManager() {
+    return delegate.getPsiManager();
+  }
+
+  @Override
+  public Project getProject() {
+    return delegate.getProject();
+  }
+
+  @Override
+  public Sdk getSdk() {
+    return delegate.getSdk();
+  }
+}
diff --git a/sdkcompat/v162/com/google/idea/sdkcompat/python/PyReferenceResolveProviderAdapter.java b/sdkcompat/v162/com/google/idea/sdkcompat/python/PyReferenceResolveProviderAdapter.java
new file mode 100644
index 0000000..36d4767
--- /dev/null
+++ b/sdkcompat/v162/com/google/idea/sdkcompat/python/PyReferenceResolveProviderAdapter.java
@@ -0,0 +1,19 @@
+package com.google.idea.sdkcompat.python;
+
+import com.jetbrains.python.psi.PyQualifiedExpression;
+import com.jetbrains.python.psi.resolve.PyReferenceResolveProvider;
+import com.jetbrains.python.psi.resolve.RatedResolveResult;
+import com.jetbrains.python.psi.types.TypeEvalContext;
+import java.util.List;
+
+/** Adapter to bridge different SDK versions. */
+public interface PyReferenceResolveProviderAdapter extends PyReferenceResolveProvider {
+
+  @Override
+  default List<RatedResolveResult> resolveName(PyQualifiedExpression element) {
+    TypeEvalContext context = TypeEvalContext.codeInsightFallback(element.getProject());
+    return resolveName(element, context);
+  }
+
+  List<RatedResolveResult> resolveName(PyQualifiedExpression element, TypeEvalContext context);
+}
diff --git a/sdkcompat/v162/com/google/idea/sdkcompat/python/ResolveImportCompatUtils.java b/sdkcompat/v162/com/google/idea/sdkcompat/python/ResolveImportCompatUtils.java
new file mode 100644
index 0000000..5a6bef2
--- /dev/null
+++ b/sdkcompat/v162/com/google/idea/sdkcompat/python/ResolveImportCompatUtils.java
@@ -0,0 +1,22 @@
+package com.google.idea.sdkcompat.python;
+
+import com.intellij.psi.PsiElement;
+import com.intellij.psi.PsiFile;
+import com.jetbrains.python.psi.resolve.ResolveImportUtil;
+import javax.annotation.Nullable;
+
+/** Handles changes to {@link ResolveImportUtil} between our supported versions. */
+public class ResolveImportCompatUtils {
+
+  @Nullable
+  public static PsiElement resolveChild(
+      @Nullable PsiElement parent,
+      String referencedName,
+      @Nullable PsiFile containingFile,
+      boolean fileOnly,
+      boolean checkForPackage,
+      boolean withoutStubs) {
+    return ResolveImportUtil.resolveChild(
+        parent, referencedName, containingFile, fileOnly, checkForPackage);
+  }
+}
diff --git a/sdkcompat/v162/com/google/idea/sdkcompat/vcs/MergeDataBuilder.java b/sdkcompat/v162/com/google/idea/sdkcompat/vcs/MergeDataBuilder.java
new file mode 100644
index 0000000..b0a7697
--- /dev/null
+++ b/sdkcompat/v162/com/google/idea/sdkcompat/vcs/MergeDataBuilder.java
@@ -0,0 +1,54 @@
+package com.google.idea.sdkcompat.vcs;
+
+import com.intellij.openapi.vcs.FilePath;
+import com.intellij.openapi.vcs.history.VcsRevisionNumber;
+import com.intellij.openapi.vcs.merge.MergeData;
+import org.jetbrains.annotations.Nullable;
+
+/** SDK adapter for creating {@link MergeData}. */
+public final class MergeDataBuilder {
+  private byte[] baseContent;
+  private byte[] theirsContent;
+  private byte[] yoursContent;
+
+  @Nullable private VcsRevisionNumber theirsRevisionNumber;
+
+  public void setBaseContent(byte[] baseContent) {
+    this.baseContent = baseContent;
+  }
+
+  public void setTheirsContent(byte[] theirsContent) {
+    this.theirsContent = theirsContent;
+  }
+
+  public void setYoursContent(byte[] yoursContent) {
+    this.yoursContent = yoursContent;
+  }
+
+  public void setBaseRevisionNumber(@Nullable VcsRevisionNumber baseRevisionNumber) {}
+
+  public void setTheirsRevisionNumber(@Nullable VcsRevisionNumber theirsRevisionNumber) {
+    this.theirsRevisionNumber = theirsRevisionNumber;
+  }
+
+  public void setYoursRevisionNumber(@Nullable VcsRevisionNumber yoursRevisionNumber) {}
+
+  public void setBaseFilePath(@Nullable FilePath baseFilePath) {}
+
+  public void setTheirsFilePath(@Nullable FilePath theirsFilePath) {}
+
+  public void setYoursFilePath(@Nullable FilePath yoursFilePath) {}
+
+  public MergeData build() {
+    MergeData mergeData = new MergeData();
+
+    mergeData.ORIGINAL = baseContent;
+
+    mergeData.LAST = theirsContent;
+    mergeData.LAST_REVISION_NUMBER = theirsRevisionNumber;
+
+    mergeData.CURRENT = yoursContent;
+
+    return mergeData;
+  }
+}
diff --git a/sdkcompat/v163/BUILD b/sdkcompat/v163/BUILD
new file mode 100644
index 0000000..8e2c272
--- /dev/null
+++ b/sdkcompat/v163/BUILD
@@ -0,0 +1,34 @@
+# Description: Indirections for SDK changes to the underlying platform library.
+
+licenses(["notice"])  # Apache 2.0
+
+load("//intellij_platform_sdk:build_defs.bzl", "select_for_ide")
+
+java_library(
+    name = "v163",
+    srcs = glob([
+        "com/google/idea/sdkcompat/codestyle/**",
+        "com/google/idea/sdkcompat/smrunner/**",
+        "com/google/idea/sdkcompat/transactions/**",
+        "com/google/idea/sdkcompat/vcs/**",
+    ]) + select_for_ide(
+        android_studio = glob([
+            "com/google/idea/sdkcompat/cidr/**",
+        ]),
+        clion = glob([
+            "com/google/idea/sdkcompat/cidr/**",
+            "com/google/idea/sdkcompat/clion/**",
+        ]),
+        intellij = glob([
+            "com/google/idea/sdkcompat/python/**",
+            "com/google/idea/sdkcompat/dart/**",
+        ]),
+    ),
+    visibility = ["//sdkcompat:__pkg__"],
+    deps = [
+        "//intellij_platform_sdk:plugin_api",
+        "@jsr305_annotations//jar",
+    ] + select_for_ide(
+        intellij = ["//third_party/python"],
+    ),
+)
diff --git a/sdkcompat/v163/com/google/idea/sdkcompat/cidr/CidrCompilerSwitchesAdapter.java b/sdkcompat/v163/com/google/idea/sdkcompat/cidr/CidrCompilerSwitchesAdapter.java
new file mode 100644
index 0000000..d60a1a6
--- /dev/null
+++ b/sdkcompat/v163/com/google/idea/sdkcompat/cidr/CidrCompilerSwitchesAdapter.java
@@ -0,0 +1,20 @@
+package com.google.idea.sdkcompat.cidr;
+
+import com.jetbrains.cidr.lang.toolchains.CidrCompilerSwitches;
+import java.util.List;
+
+/** Adapter to bridge different SDK versions. */
+public class CidrCompilerSwitchesAdapter {
+  /** Old interface does not know anything about CidrCompilerSwitches.Format */
+  public static List<String> getFileArgs(CidrCompilerSwitches switches) {
+    return switches.getFileArgs();
+  }
+
+  public static List<String> getCommandLineArgs(CidrCompilerSwitches switches) {
+    return switches.getCommandLineArgs();
+  }
+
+  public static String getCommandLineString(CidrCompilerSwitches switches) {
+    return switches.getCommandLineString();
+  }
+}
diff --git a/sdkcompat/v163/com/google/idea/sdkcompat/cidr/CidrConsoleBuilderAdapter.java b/sdkcompat/v163/com/google/idea/sdkcompat/cidr/CidrConsoleBuilderAdapter.java
new file mode 100644
index 0000000..70e753b
--- /dev/null
+++ b/sdkcompat/v163/com/google/idea/sdkcompat/cidr/CidrConsoleBuilderAdapter.java
@@ -0,0 +1,12 @@
+package com.google.idea.sdkcompat.cidr;
+
+import com.intellij.openapi.project.Project;
+import com.jetbrains.cidr.execution.CidrConsoleBuilder;
+
+/** Adapter to bridge different SDK versions. */
+public class CidrConsoleBuilderAdapter extends CidrConsoleBuilder {
+
+  public CidrConsoleBuilderAdapter(Project project) {
+    super(project, null, null);
+  }
+}
diff --git a/sdkcompat/v163/com/google/idea/sdkcompat/cidr/CidrGoogleTestUtilAdapter.java b/sdkcompat/v163/com/google/idea/sdkcompat/cidr/CidrGoogleTestUtilAdapter.java
new file mode 100644
index 0000000..cf3a258
--- /dev/null
+++ b/sdkcompat/v163/com/google/idea/sdkcompat/cidr/CidrGoogleTestUtilAdapter.java
@@ -0,0 +1,6 @@
+package com.google.idea.sdkcompat.cidr;
+
+import com.jetbrains.cidr.execution.testing.CidrTestUtil;
+
+/** Adapter to bridge different SDK versions. */
+public class CidrGoogleTestUtilAdapter extends CidrTestUtil {}
diff --git a/sdkcompat/v163/com/google/idea/sdkcompat/cidr/CidrSwitchBuilderAdapter.java b/sdkcompat/v163/com/google/idea/sdkcompat/cidr/CidrSwitchBuilderAdapter.java
new file mode 100644
index 0000000..68772fa
--- /dev/null
+++ b/sdkcompat/v163/com/google/idea/sdkcompat/cidr/CidrSwitchBuilderAdapter.java
@@ -0,0 +1,24 @@
+package com.google.idea.sdkcompat.cidr;
+
+import com.google.common.base.Joiner;
+import com.jetbrains.cidr.lang.toolchains.CidrSwitchBuilder;
+import java.util.List;
+import java.util.stream.Collectors;
+
+/** Adapter to bridge different SDK versions. */
+public class CidrSwitchBuilderAdapter extends CidrSwitchBuilder {
+  /**
+   * Old CidrSwitchBuilder is unable to deal with options with spaces embedded. This is a hack to
+   * preserve the old behaviour for 2016.3 Original hack explanation: - this list of switches is
+   * currently only used in one place -- GCCCompiler.tryRunGCC. - list is written to an argument
+   * file, whitespace-separated, then passed as a @file arg to clang. In this context, escaped
+   * whitespace within a single arg is not handled. Currently, the only way (short of using
+   * reflection) to ensure unescaped whitespace is to have CidrSwitchBuilder treat whitespace as a
+   * delimiter between args.
+   */
+  public CidrSwitchBuilderAdapter addAllRaw(List<String> switches) {
+    switches = switches.stream().map(flag -> flag.replace("\\ ", " ")).collect(Collectors.toList());
+    addAll(Joiner.on(" ").join(switches), CidrSwitchBuilder.Format.FILE_ARGS);
+    return this;
+  }
+}
diff --git a/sdkcompat/v163/com/google/idea/sdkcompat/cidr/OCResolveConfigurationAdapter.java b/sdkcompat/v163/com/google/idea/sdkcompat/cidr/OCResolveConfigurationAdapter.java
new file mode 100644
index 0000000..912eb77
--- /dev/null
+++ b/sdkcompat/v163/com/google/idea/sdkcompat/cidr/OCResolveConfigurationAdapter.java
@@ -0,0 +1,20 @@
+package com.google.idea.sdkcompat.cidr;
+
+import com.intellij.openapi.vfs.VirtualFile;
+import com.jetbrains.cidr.lang.OCLanguageKind;
+import com.jetbrains.cidr.lang.workspace.OCResolveConfiguration;
+import java.util.Collection;
+import java.util.List;
+import java.util.Set;
+
+/** Adapter to bridge different SDK versions. */
+public interface OCResolveConfigurationAdapter extends OCResolveConfiguration {
+  /* v171 */
+  public List<VirtualFile> getPrecompiledHeaders(OCLanguageKind kind, VirtualFile sourceFile);
+
+  /* v171 */
+  public Collection<VirtualFile> getSources();
+
+  /* v171 */
+  public Set<VirtualFile> getPrecompiledHeaders();
+}
diff --git a/sdkcompat/v163/com/google/idea/sdkcompat/cidr/OCWorkspaceAdapter.java b/sdkcompat/v163/com/google/idea/sdkcompat/cidr/OCWorkspaceAdapter.java
new file mode 100644
index 0000000..4266e35
--- /dev/null
+++ b/sdkcompat/v163/com/google/idea/sdkcompat/cidr/OCWorkspaceAdapter.java
@@ -0,0 +1,9 @@
+package com.google.idea.sdkcompat.cidr;
+
+import com.intellij.openapi.project.Project;
+import com.jetbrains.cidr.lang.workspace.OCWorkspace;
+
+/** Adapter to bridge different SDK versions. */
+public abstract class OCWorkspaceAdapter implements OCWorkspace {
+  protected OCWorkspaceAdapter(Project project) {}
+}
diff --git a/sdkcompat/v163/com/google/idea/sdkcompat/cidr/OCWorkspaceModificationTrackersCompatUtils.java b/sdkcompat/v163/com/google/idea/sdkcompat/cidr/OCWorkspaceModificationTrackersCompatUtils.java
new file mode 100644
index 0000000..1f38a39
--- /dev/null
+++ b/sdkcompat/v163/com/google/idea/sdkcompat/cidr/OCWorkspaceModificationTrackersCompatUtils.java
@@ -0,0 +1,21 @@
+package com.google.idea.sdkcompat.cidr;
+
+import com.intellij.openapi.project.Project;
+import com.jetbrains.cidr.lang.workspace.OCWorkspaceModificationTrackers;
+
+/** Handles changes to modification trackers between our supported versions. */
+public class OCWorkspaceModificationTrackersCompatUtils {
+
+  public static OCWorkspaceModificationTrackers getTrackers(Project project) {
+    return OCWorkspaceModificationTrackers.getInstance(project);
+  }
+
+  /** Must be called inside a write action, on the EDT. */
+  public static void incrementModificationCounts(Project project) {
+    OCWorkspaceModificationTrackers modTrackers = getTrackers(project);
+    modTrackers.getProjectFilesListTracker().incModificationCount();
+    modTrackers.getSourceFilesListTracker().incModificationCount();
+    modTrackers.getSelectedResolveConfigurationTracker().incModificationCount();
+    modTrackers.getBuildSettingsChangesTracker().incModificationCount();
+  }
+}
diff --git a/sdkcompat/v163/com/google/idea/sdkcompat/clion/CMakeActionList.java b/sdkcompat/v163/com/google/idea/sdkcompat/clion/CMakeActionList.java
new file mode 100644
index 0000000..55f4936
--- /dev/null
+++ b/sdkcompat/v163/com/google/idea/sdkcompat/clion/CMakeActionList.java
@@ -0,0 +1,22 @@
+package com.google.idea.sdkcompat.clion;
+
+import com.google.common.collect.ImmutableSet;
+import com.jetbrains.cidr.cpp.cmake.actions.ChangeCMakeProjectContentRootAction;
+import com.jetbrains.cidr.cpp.cmake.actions.ClearCMakeCacheAndReloadAction;
+import com.jetbrains.cidr.cpp.cmake.actions.OpenCMakeSettingsAction;
+import com.jetbrains.cidr.cpp.cmake.actions.ReloadCMakeProjectAction;
+import com.jetbrains.cidr.cpp.cmake.actions.ToggleCMakeAutoReloadAction;
+
+/** Handles CMake actions which have changed between our supported versions. */
+public class CMakeActionList {
+
+  public static final ImmutableSet<String> CMAKE_ACTION_IDS =
+      ImmutableSet.of(
+          ChangeCMakeProjectContentRootAction.ID,
+          ClearCMakeCacheAndReloadAction.ID,
+          OpenCMakeSettingsAction.ID,
+          ReloadCMakeProjectAction.ID,
+          ToggleCMakeAutoReloadAction.ID,
+          // 'CMake' > 'Show Generated CMake Files' action
+          "CMake.ShowGeneratedDir");
+}
diff --git a/sdkcompat/v163/com/google/idea/sdkcompat/clion/CMakeConfigurationProducersList.java b/sdkcompat/v163/com/google/idea/sdkcompat/clion/CMakeConfigurationProducersList.java
new file mode 100644
index 0000000..f402269
--- /dev/null
+++ b/sdkcompat/v163/com/google/idea/sdkcompat/clion/CMakeConfigurationProducersList.java
@@ -0,0 +1,11 @@
+package com.google.idea.sdkcompat.clion;
+
+import com.google.common.collect.ImmutableList;
+import com.intellij.execution.actions.RunConfigurationProducer;
+import com.jetbrains.cidr.cpp.execution.testing.CMakeGoogleTestRunConfigurationProducer;
+
+/** List of C/C++ configuration producers for a given plugin version. */
+public class CMakeConfigurationProducersList {
+  public static final ImmutableList<Class<? extends RunConfigurationProducer<?>>>
+      PRODUCERS_TO_SUPPRESS = ImmutableList.of(CMakeGoogleTestRunConfigurationProducer.class);
+}
diff --git a/sdkcompat/v163/com/google/idea/sdkcompat/codestyle/CodeStyleManagerSdkCompatAdapter.java b/sdkcompat/v163/com/google/idea/sdkcompat/codestyle/DelegatingCodeStyleManagerSdkCompatAdapter.java
similarity index 70%
rename from sdkcompat/v163/com/google/idea/sdkcompat/codestyle/CodeStyleManagerSdkCompatAdapter.java
rename to sdkcompat/v163/com/google/idea/sdkcompat/codestyle/DelegatingCodeStyleManagerSdkCompatAdapter.java
index 33d3ad4..8075c2e 100644
--- a/sdkcompat/v163/com/google/idea/sdkcompat/codestyle/CodeStyleManagerSdkCompatAdapter.java
+++ b/sdkcompat/v163/com/google/idea/sdkcompat/codestyle/DelegatingCodeStyleManagerSdkCompatAdapter.java
@@ -9,8 +9,15 @@
 import java.util.List;
 import org.jetbrains.annotations.NotNull;
 
-/** Adapter to extend two bridge different IntelliJ SDK versions. */
-public abstract class CodeStyleManagerSdkCompatAdapter extends CodeStyleManager {
+/** Adapter to bridge different SDK versions. */
+public abstract class DelegatingCodeStyleManagerSdkCompatAdapter extends CodeStyleManager {
+
+  protected CodeStyleManager delegate;
+
+  protected DelegatingCodeStyleManagerSdkCompatAdapter(CodeStyleManager delegate) {
+    this.delegate = delegate;
+  }
+
   @Override
   public void reformatTextWithContext(@NotNull PsiFile file, @NotNull ChangedRangesInfo info)
       throws IncorrectOperationException {
diff --git a/sdkcompat/v163/com/google/idea/sdkcompat/dart/DartSdkCompatUtils.java b/sdkcompat/v163/com/google/idea/sdkcompat/dart/DartSdkCompatUtils.java
new file mode 100644
index 0000000..58f48e4
--- /dev/null
+++ b/sdkcompat/v163/com/google/idea/sdkcompat/dart/DartSdkCompatUtils.java
@@ -0,0 +1,32 @@
+/*
+ * Copyright 2017 The Bazel Authors. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.idea.sdkcompat.dart;
+
+import com.intellij.openapi.project.Project;
+import com.intellij.openapi.roots.impl.libraries.ApplicationLibraryTable;
+import com.intellij.openapi.roots.libraries.Library;
+import javax.annotation.Nullable;
+
+/** Handles changes to the Dart plugin libraries between our supported versions. */
+public class DartSdkCompatUtils {
+
+  public static final String DART_SDK_LIBRARY_NAME = "Dart SDK";
+
+  @Nullable
+  public static Library findDartLibrary(Project project) {
+    return ApplicationLibraryTable.getApplicationTable().getLibraryByName(DART_SDK_LIBRARY_NAME);
+  }
+}
diff --git a/sdkcompat/v163/com/google/idea/sdkcompat/debugger/GenericDebuggerRunnerSdkCompatAdapter.java b/sdkcompat/v163/com/google/idea/sdkcompat/debugger/GenericDebuggerRunnerSdkCompatAdapter.java
deleted file mode 100644
index ed822b3..0000000
--- a/sdkcompat/v163/com/google/idea/sdkcompat/debugger/GenericDebuggerRunnerSdkCompatAdapter.java
+++ /dev/null
@@ -1,6 +0,0 @@
-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/python/PyConfigurationProducersList.java b/sdkcompat/v163/com/google/idea/sdkcompat/python/PyConfigurationProducersList.java
new file mode 100644
index 0000000..b3785da
--- /dev/null
+++ b/sdkcompat/v163/com/google/idea/sdkcompat/python/PyConfigurationProducersList.java
@@ -0,0 +1,17 @@
+package com.google.idea.sdkcompat.python;
+
+import com.google.common.collect.ImmutableList;
+import java.util.Collection;
+
+/** List of python configuration producers for a given plugin version. */
+public class PyConfigurationProducersList {
+
+  public static final Collection<Class<?>> PRODUCERS_TO_SUPPRESS =
+      ImmutableList.of(
+          com.jetbrains.python.run.PythonRunConfigurationProducer.class,
+          com.jetbrains.python.testing.attest.PythonAtTestConfigurationProducer.class,
+          com.jetbrains.python.testing.nosetest.PythonNoseTestConfigurationProducer.class,
+          com.jetbrains.python.testing.doctest.PythonDocTestConfigurationProducer.class,
+          com.jetbrains.python.testing.pytest.PyTestConfigurationProducer.class,
+          com.jetbrains.python.testing.unittest.PythonUnitTestConfigurationProducer.class);
+}
diff --git a/sdkcompat/v163/com/google/idea/sdkcompat/python/PyImportResolverAdapter.java b/sdkcompat/v163/com/google/idea/sdkcompat/python/PyImportResolverAdapter.java
new file mode 100644
index 0000000..9ccba03
--- /dev/null
+++ b/sdkcompat/v163/com/google/idea/sdkcompat/python/PyImportResolverAdapter.java
@@ -0,0 +1,23 @@
+package com.google.idea.sdkcompat.python;
+
+import com.intellij.psi.PsiElement;
+import com.intellij.psi.util.QualifiedName;
+import com.jetbrains.python.psi.impl.PyImportResolver;
+import com.jetbrains.python.psi.resolve.QualifiedNameResolveContext;
+import javax.annotation.Nullable;
+
+/** Adapter to bridge different SDK versions. */
+public interface PyImportResolverAdapter extends PyImportResolver {
+
+  @Nullable
+  PsiElement resolveImportReference(
+      QualifiedName name, PyQualifiedNameResolveContextAdapter context, boolean withRoots);
+
+  @Override
+  @Nullable
+  default PsiElement resolveImportReference(
+      QualifiedName name, QualifiedNameResolveContext context, boolean withRoots) {
+    return resolveImportReference(
+        name, new PyQualifiedNameResolveContextAdapter(context), withRoots);
+  }
+}
diff --git a/sdkcompat/v163/com/google/idea/sdkcompat/python/PyQualifiedNameResolveContextAdapter.java b/sdkcompat/v163/com/google/idea/sdkcompat/python/PyQualifiedNameResolveContextAdapter.java
new file mode 100644
index 0000000..7330e11
--- /dev/null
+++ b/sdkcompat/v163/com/google/idea/sdkcompat/python/PyQualifiedNameResolveContextAdapter.java
@@ -0,0 +1,77 @@
+package com.google.idea.sdkcompat.python;
+
+import com.intellij.openapi.module.Module;
+import com.intellij.openapi.project.Project;
+import com.intellij.openapi.projectRoots.Sdk;
+import com.intellij.psi.PsiElement;
+import com.intellij.psi.PsiFile;
+import com.intellij.psi.PsiManager;
+import com.jetbrains.python.psi.resolve.QualifiedNameResolveContext;
+import javax.annotation.Nullable;
+
+/** Adapter to bridge different SDK versions. */
+public class PyQualifiedNameResolveContextAdapter extends QualifiedNameResolveContext {
+
+  private final QualifiedNameResolveContext delegate;
+
+  PyQualifiedNameResolveContextAdapter(QualifiedNameResolveContext delegate) {
+    this.delegate = delegate;
+  }
+
+  @Override
+  public void copyFrom(QualifiedNameResolveContext context) {
+    delegate.copyFrom(context);
+  }
+
+  @Override
+  public void setFromElement(PsiElement element) {
+    delegate.setFromElement(element);
+  }
+
+  @Override
+  public void setFromModule(Module module) {
+    delegate.setFromModule(module);
+  }
+
+  @Override
+  public void setFromSdk(Project project, Sdk sdk) {
+    delegate.setFromSdk(project, sdk);
+  }
+
+  @Override
+  public void setSdk(Sdk sdk) {
+    delegate.setSdk(sdk);
+  }
+
+  @Override
+  @Nullable
+  public Module getModule() {
+    return delegate.getModule();
+  }
+
+  @Override
+  public boolean isValid() {
+    return delegate.isValid();
+  }
+
+  @Override
+  @Nullable
+  public PsiFile getFootholdFile() {
+    return delegate.getFootholdFile();
+  }
+
+  @Override
+  public PsiManager getPsiManager() {
+    return delegate.getPsiManager();
+  }
+
+  @Override
+  public Project getProject() {
+    return delegate.getProject();
+  }
+
+  @Override
+  public Sdk getSdk() {
+    return delegate.getSdk();
+  }
+}
diff --git a/sdkcompat/v163/com/google/idea/sdkcompat/python/PyReferenceResolveProviderAdapter.java b/sdkcompat/v163/com/google/idea/sdkcompat/python/PyReferenceResolveProviderAdapter.java
new file mode 100644
index 0000000..36d4767
--- /dev/null
+++ b/sdkcompat/v163/com/google/idea/sdkcompat/python/PyReferenceResolveProviderAdapter.java
@@ -0,0 +1,19 @@
+package com.google.idea.sdkcompat.python;
+
+import com.jetbrains.python.psi.PyQualifiedExpression;
+import com.jetbrains.python.psi.resolve.PyReferenceResolveProvider;
+import com.jetbrains.python.psi.resolve.RatedResolveResult;
+import com.jetbrains.python.psi.types.TypeEvalContext;
+import java.util.List;
+
+/** Adapter to bridge different SDK versions. */
+public interface PyReferenceResolveProviderAdapter extends PyReferenceResolveProvider {
+
+  @Override
+  default List<RatedResolveResult> resolveName(PyQualifiedExpression element) {
+    TypeEvalContext context = TypeEvalContext.codeInsightFallback(element.getProject());
+    return resolveName(element, context);
+  }
+
+  List<RatedResolveResult> resolveName(PyQualifiedExpression element, TypeEvalContext context);
+}
diff --git a/sdkcompat/v163/com/google/idea/sdkcompat/python/ResolveImportCompatUtils.java b/sdkcompat/v163/com/google/idea/sdkcompat/python/ResolveImportCompatUtils.java
new file mode 100644
index 0000000..5a6bef2
--- /dev/null
+++ b/sdkcompat/v163/com/google/idea/sdkcompat/python/ResolveImportCompatUtils.java
@@ -0,0 +1,22 @@
+package com.google.idea.sdkcompat.python;
+
+import com.intellij.psi.PsiElement;
+import com.intellij.psi.PsiFile;
+import com.jetbrains.python.psi.resolve.ResolveImportUtil;
+import javax.annotation.Nullable;
+
+/** Handles changes to {@link ResolveImportUtil} between our supported versions. */
+public class ResolveImportCompatUtils {
+
+  @Nullable
+  public static PsiElement resolveChild(
+      @Nullable PsiElement parent,
+      String referencedName,
+      @Nullable PsiFile containingFile,
+      boolean fileOnly,
+      boolean checkForPackage,
+      boolean withoutStubs) {
+    return ResolveImportUtil.resolveChild(
+        parent, referencedName, containingFile, fileOnly, checkForPackage);
+  }
+}
diff --git a/sdkcompat/v163/com/google/idea/sdkcompat/vcs/MergeDataBuilder.java b/sdkcompat/v163/com/google/idea/sdkcompat/vcs/MergeDataBuilder.java
new file mode 100644
index 0000000..b0a7697
--- /dev/null
+++ b/sdkcompat/v163/com/google/idea/sdkcompat/vcs/MergeDataBuilder.java
@@ -0,0 +1,54 @@
+package com.google.idea.sdkcompat.vcs;
+
+import com.intellij.openapi.vcs.FilePath;
+import com.intellij.openapi.vcs.history.VcsRevisionNumber;
+import com.intellij.openapi.vcs.merge.MergeData;
+import org.jetbrains.annotations.Nullable;
+
+/** SDK adapter for creating {@link MergeData}. */
+public final class MergeDataBuilder {
+  private byte[] baseContent;
+  private byte[] theirsContent;
+  private byte[] yoursContent;
+
+  @Nullable private VcsRevisionNumber theirsRevisionNumber;
+
+  public void setBaseContent(byte[] baseContent) {
+    this.baseContent = baseContent;
+  }
+
+  public void setTheirsContent(byte[] theirsContent) {
+    this.theirsContent = theirsContent;
+  }
+
+  public void setYoursContent(byte[] yoursContent) {
+    this.yoursContent = yoursContent;
+  }
+
+  public void setBaseRevisionNumber(@Nullable VcsRevisionNumber baseRevisionNumber) {}
+
+  public void setTheirsRevisionNumber(@Nullable VcsRevisionNumber theirsRevisionNumber) {
+    this.theirsRevisionNumber = theirsRevisionNumber;
+  }
+
+  public void setYoursRevisionNumber(@Nullable VcsRevisionNumber yoursRevisionNumber) {}
+
+  public void setBaseFilePath(@Nullable FilePath baseFilePath) {}
+
+  public void setTheirsFilePath(@Nullable FilePath theirsFilePath) {}
+
+  public void setYoursFilePath(@Nullable FilePath yoursFilePath) {}
+
+  public MergeData build() {
+    MergeData mergeData = new MergeData();
+
+    mergeData.ORIGINAL = baseContent;
+
+    mergeData.LAST = theirsContent;
+    mergeData.LAST_REVISION_NUMBER = theirsRevisionNumber;
+
+    mergeData.CURRENT = yoursContent;
+
+    return mergeData;
+  }
+}
diff --git a/sdkcompat/v171/BUILD b/sdkcompat/v171/BUILD
new file mode 100644
index 0000000..d8392a9
--- /dev/null
+++ b/sdkcompat/v171/BUILD
@@ -0,0 +1,33 @@
+# Description: Indirections for SDK changes to the underlying platform library.
+
+licenses(["notice"])  # Apache 2.0
+
+load("//intellij_platform_sdk:build_defs.bzl", "select_for_ide")
+
+java_library(
+    name = "v171",
+    srcs = glob([
+        "com/google/idea/sdkcompat/codestyle/**",
+        "com/google/idea/sdkcompat/python/**",
+        "com/google/idea/sdkcompat/smrunner/**",
+        "com/google/idea/sdkcompat/transactions/**",
+        "com/google/idea/sdkcompat/vcs/**",
+    ]) + select_for_ide(
+        android_studio = glob([
+            "com/google/idea/sdkcompat/cidr/**",
+        ]),
+        clion = glob([
+            "com/google/idea/sdkcompat/cidr/**",
+            "com/google/idea/sdkcompat/clion/**",
+        ]),
+        intellij = glob([
+            "com/google/idea/sdkcompat/dart/**",
+        ]),
+    ),
+    visibility = ["//sdkcompat:__pkg__"],
+    deps = [
+        "//intellij_platform_sdk:plugin_api",
+        "//third_party/python",
+        "@jsr305_annotations//jar",
+    ],
+)
diff --git a/sdkcompat/v171/com/google/idea/sdkcompat/cidr/CidrCompilerSwitchesAdapter.java b/sdkcompat/v171/com/google/idea/sdkcompat/cidr/CidrCompilerSwitchesAdapter.java
new file mode 100644
index 0000000..c20ad1f
--- /dev/null
+++ b/sdkcompat/v171/com/google/idea/sdkcompat/cidr/CidrCompilerSwitchesAdapter.java
@@ -0,0 +1,21 @@
+package com.google.idea.sdkcompat.cidr;
+
+import com.intellij.openapi.util.text.StringUtil;
+import com.jetbrains.cidr.lang.toolchains.CidrCompilerSwitches;
+import java.util.List;
+
+/** Adapter to bridge different SDK versions. */
+public class CidrCompilerSwitchesAdapter {
+  /** Old interface does not know anything about CidrCompilerSwitches.Format */
+  public static List<String> getFileArgs(CidrCompilerSwitches switches) {
+    return switches.getList(CidrCompilerSwitches.Format.RAW);
+  }
+
+  public static List<String> getCommandLineArgs(CidrCompilerSwitches switches) {
+    return switches.getList(CidrCompilerSwitches.Format.BASH_SHELL);
+  }
+
+  public static String getCommandLineString(CidrCompilerSwitches switches) {
+    return StringUtil.join(getCommandLineArgs(switches), " ");
+  }
+}
diff --git a/sdkcompat/v171/com/google/idea/sdkcompat/cidr/CidrConsoleBuilderAdapter.java b/sdkcompat/v171/com/google/idea/sdkcompat/cidr/CidrConsoleBuilderAdapter.java
new file mode 100644
index 0000000..70e753b
--- /dev/null
+++ b/sdkcompat/v171/com/google/idea/sdkcompat/cidr/CidrConsoleBuilderAdapter.java
@@ -0,0 +1,12 @@
+package com.google.idea.sdkcompat.cidr;
+
+import com.intellij.openapi.project.Project;
+import com.jetbrains.cidr.execution.CidrConsoleBuilder;
+
+/** Adapter to bridge different SDK versions. */
+public class CidrConsoleBuilderAdapter extends CidrConsoleBuilder {
+
+  public CidrConsoleBuilderAdapter(Project project) {
+    super(project, null, null);
+  }
+}
diff --git a/sdkcompat/v171/com/google/idea/sdkcompat/cidr/CidrGoogleTestUtilAdapter.java b/sdkcompat/v171/com/google/idea/sdkcompat/cidr/CidrGoogleTestUtilAdapter.java
new file mode 100644
index 0000000..efad3f4
--- /dev/null
+++ b/sdkcompat/v171/com/google/idea/sdkcompat/cidr/CidrGoogleTestUtilAdapter.java
@@ -0,0 +1,6 @@
+package com.google.idea.sdkcompat.cidr;
+
+import com.jetbrains.cidr.execution.testing.google.CidrGoogleTestUtil;
+
+/** Adapter to bridge different SDK versions. */
+public class CidrGoogleTestUtilAdapter extends CidrGoogleTestUtil {}
diff --git a/sdkcompat/v171/com/google/idea/sdkcompat/cidr/CidrSwitchBuilderAdapter.java b/sdkcompat/v171/com/google/idea/sdkcompat/cidr/CidrSwitchBuilderAdapter.java
new file mode 100644
index 0000000..40684be
--- /dev/null
+++ b/sdkcompat/v171/com/google/idea/sdkcompat/cidr/CidrSwitchBuilderAdapter.java
@@ -0,0 +1,6 @@
+package com.google.idea.sdkcompat.cidr;
+
+import com.jetbrains.cidr.lang.toolchains.CidrSwitchBuilder;
+
+/** Adapter to bridge different SDK versions. */
+public class CidrSwitchBuilderAdapter extends CidrSwitchBuilder {}
diff --git a/sdkcompat/v171/com/google/idea/sdkcompat/cidr/OCResolveConfigurationAdapter.java b/sdkcompat/v171/com/google/idea/sdkcompat/cidr/OCResolveConfigurationAdapter.java
new file mode 100644
index 0000000..a6bf252
--- /dev/null
+++ b/sdkcompat/v171/com/google/idea/sdkcompat/cidr/OCResolveConfigurationAdapter.java
@@ -0,0 +1,14 @@
+package com.google.idea.sdkcompat.cidr;
+
+import com.intellij.openapi.vfs.VirtualFile;
+import com.jetbrains.cidr.lang.OCLanguageKind;
+import com.jetbrains.cidr.lang.workspace.OCResolveConfiguration;
+
+/** Adapter to bridge different SDK versions. */
+public interface OCResolveConfigurationAdapter extends OCResolveConfiguration {
+  /* v162/v163 */
+  public VirtualFile getPrecompiledHeader();
+
+  /* v162/v163 */
+  public OCLanguageKind getPrecompiledLanguageKind();
+}
diff --git a/sdkcompat/v171/com/google/idea/sdkcompat/cidr/OCWorkspaceAdapter.java b/sdkcompat/v171/com/google/idea/sdkcompat/cidr/OCWorkspaceAdapter.java
new file mode 100644
index 0000000..4266e35
--- /dev/null
+++ b/sdkcompat/v171/com/google/idea/sdkcompat/cidr/OCWorkspaceAdapter.java
@@ -0,0 +1,9 @@
+package com.google.idea.sdkcompat.cidr;
+
+import com.intellij.openapi.project.Project;
+import com.jetbrains.cidr.lang.workspace.OCWorkspace;
+
+/** Adapter to bridge different SDK versions. */
+public abstract class OCWorkspaceAdapter implements OCWorkspace {
+  protected OCWorkspaceAdapter(Project project) {}
+}
diff --git a/sdkcompat/v171/com/google/idea/sdkcompat/cidr/OCWorkspaceModificationTrackersCompatUtils.java b/sdkcompat/v171/com/google/idea/sdkcompat/cidr/OCWorkspaceModificationTrackersCompatUtils.java
new file mode 100644
index 0000000..1f38a39
--- /dev/null
+++ b/sdkcompat/v171/com/google/idea/sdkcompat/cidr/OCWorkspaceModificationTrackersCompatUtils.java
@@ -0,0 +1,21 @@
+package com.google.idea.sdkcompat.cidr;
+
+import com.intellij.openapi.project.Project;
+import com.jetbrains.cidr.lang.workspace.OCWorkspaceModificationTrackers;
+
+/** Handles changes to modification trackers between our supported versions. */
+public class OCWorkspaceModificationTrackersCompatUtils {
+
+  public static OCWorkspaceModificationTrackers getTrackers(Project project) {
+    return OCWorkspaceModificationTrackers.getInstance(project);
+  }
+
+  /** Must be called inside a write action, on the EDT. */
+  public static void incrementModificationCounts(Project project) {
+    OCWorkspaceModificationTrackers modTrackers = getTrackers(project);
+    modTrackers.getProjectFilesListTracker().incModificationCount();
+    modTrackers.getSourceFilesListTracker().incModificationCount();
+    modTrackers.getSelectedResolveConfigurationTracker().incModificationCount();
+    modTrackers.getBuildSettingsChangesTracker().incModificationCount();
+  }
+}
diff --git a/sdkcompat/v171/com/google/idea/sdkcompat/clion/CMakeActionList.java b/sdkcompat/v171/com/google/idea/sdkcompat/clion/CMakeActionList.java
new file mode 100644
index 0000000..1230fd0
--- /dev/null
+++ b/sdkcompat/v171/com/google/idea/sdkcompat/clion/CMakeActionList.java
@@ -0,0 +1,23 @@
+package com.google.idea.sdkcompat.clion;
+
+import com.google.common.collect.ImmutableSet;
+import com.jetbrains.cidr.cpp.cmake.actions.ChangeCMakeProjectContentRootAction;
+import com.jetbrains.cidr.cpp.cmake.actions.ClearCMakeCacheAndReloadAction;
+import com.jetbrains.cidr.cpp.cmake.actions.ReloadCMakeProjectAction;
+import com.jetbrains.cidr.cpp.cmake.actions.ToggleCMakeAutoReloadAction;
+
+/** Handles CMake actions which have changed between our supported versions. */
+public class CMakeActionList {
+
+  public static final ImmutableSet<String> CMAKE_ACTION_IDS =
+      ImmutableSet.of(
+          ChangeCMakeProjectContentRootAction.ID,
+          ClearCMakeCacheAndReloadAction.ID,
+          // 'CMake' -> 'CMake Settings' action: com.cidr.cpp.cmake.actions.OpenCMakeSettingsAction
+          "CMake.OpenCMakeSettings",
+          ReloadCMakeProjectAction.ID,
+          ToggleCMakeAutoReloadAction.ID,
+          // 'CMake' > 'Show Generated CMake Files' action:
+          //   com.cidr.cpp.cmake.actions.ShowCMakeGeneratedDirAction
+          "CMake.ShowCMakeGeneratedDir");
+}
diff --git a/sdkcompat/v171/com/google/idea/sdkcompat/clion/CMakeConfigurationProducersList.java b/sdkcompat/v171/com/google/idea/sdkcompat/clion/CMakeConfigurationProducersList.java
new file mode 100644
index 0000000..abc58af
--- /dev/null
+++ b/sdkcompat/v171/com/google/idea/sdkcompat/clion/CMakeConfigurationProducersList.java
@@ -0,0 +1,16 @@
+package com.google.idea.sdkcompat.clion;
+
+import com.google.common.collect.ImmutableList;
+import com.intellij.execution.actions.RunConfigurationProducer;
+import com.jetbrains.cidr.cpp.execution.testing.google.CMakeGoogleTestRunConfigurationProducer;
+import com.jetbrains.cidr.cpp.execution.testing.tcatch.CMakeCatchTestRunConfigurationProducer;
+
+/** List of C/C++ configuration producers for a given plugin version. */
+public class CMakeConfigurationProducersList {
+
+  public static final ImmutableList<Class<? extends RunConfigurationProducer<?>>>
+      PRODUCERS_TO_SUPPRESS =
+          ImmutableList.of(
+              CMakeGoogleTestRunConfigurationProducer.class,
+              CMakeCatchTestRunConfigurationProducer.class);
+}
diff --git a/sdkcompat/v171/com/google/idea/sdkcompat/codestyle/DelegatingCodeStyleManagerSdkCompatAdapter.java b/sdkcompat/v171/com/google/idea/sdkcompat/codestyle/DelegatingCodeStyleManagerSdkCompatAdapter.java
new file mode 100644
index 0000000..d6cbc72
--- /dev/null
+++ b/sdkcompat/v171/com/google/idea/sdkcompat/codestyle/DelegatingCodeStyleManagerSdkCompatAdapter.java
@@ -0,0 +1,53 @@
+package com.google.idea.sdkcompat.codestyle;
+
+import com.intellij.formatting.FormattingMode;
+import com.intellij.openapi.editor.Document;
+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.psi.codeStyle.FormattingModeAwareIndentAdjuster;
+import com.intellij.util.IncorrectOperationException;
+import java.util.ArrayList;
+import java.util.List;
+import org.jetbrains.annotations.NotNull;
+
+/** Adapter to bridge different SDK versions. */
+public abstract class DelegatingCodeStyleManagerSdkCompatAdapter extends CodeStyleManager
+    implements FormattingModeAwareIndentAdjuster {
+
+  protected CodeStyleManager delegate;
+
+  protected DelegatingCodeStyleManagerSdkCompatAdapter(CodeStyleManager delegate) {
+    this.delegate = delegate;
+  }
+
+  @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);
+  }
+
+  /** Uses same fallback as {@link CodeStyleManager#getCurrentFormattingMode}. */
+  @Override
+  public FormattingMode getCurrentFormattingMode() {
+    if (delegate instanceof FormattingModeAwareIndentAdjuster) {
+      return ((FormattingModeAwareIndentAdjuster) delegate).getCurrentFormattingMode();
+    }
+    return FormattingMode.REFORMAT;
+  }
+
+  @Override
+  public int adjustLineIndent(
+      @NotNull final Document document, final int offset, FormattingMode mode)
+      throws IncorrectOperationException {
+    if (delegate instanceof FormattingModeAwareIndentAdjuster) {
+      return ((FormattingModeAwareIndentAdjuster) delegate)
+          .adjustLineIndent(document, offset, mode);
+    }
+    return offset;
+  }
+}
diff --git a/sdkcompat/v171/com/google/idea/sdkcompat/dart/DartSdkCompatUtils.java b/sdkcompat/v171/com/google/idea/sdkcompat/dart/DartSdkCompatUtils.java
new file mode 100644
index 0000000..c2d1a3e
--- /dev/null
+++ b/sdkcompat/v171/com/google/idea/sdkcompat/dart/DartSdkCompatUtils.java
@@ -0,0 +1,32 @@
+/*
+ * Copyright 2017 The Bazel Authors. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.idea.sdkcompat.dart;
+
+import com.intellij.openapi.project.Project;
+import com.intellij.openapi.roots.impl.libraries.ProjectLibraryTable;
+import com.intellij.openapi.roots.libraries.Library;
+import javax.annotation.Nullable;
+
+/** Handles changes to the Dart plugin libraries between our supported versions. */
+public class DartSdkCompatUtils {
+
+  public static final String DART_SDK_LIBRARY_NAME = "Dart SDK";
+
+  @Nullable
+  public static Library findDartLibrary(Project project) {
+    return ProjectLibraryTable.getInstance(project).getLibraryByName(DART_SDK_LIBRARY_NAME);
+  }
+}
diff --git a/sdkcompat/v171/com/google/idea/sdkcompat/python/PyConfigurationProducersList.java b/sdkcompat/v171/com/google/idea/sdkcompat/python/PyConfigurationProducersList.java
new file mode 100644
index 0000000..e65ad6e
--- /dev/null
+++ b/sdkcompat/v171/com/google/idea/sdkcompat/python/PyConfigurationProducersList.java
@@ -0,0 +1,17 @@
+package com.google.idea.sdkcompat.python;
+
+import com.google.common.collect.ImmutableList;
+import java.util.Collection;
+
+/** List of python configuration producers for a given plugin version. */
+public class PyConfigurationProducersList {
+
+  public static final Collection<Class<?>> PRODUCERS_TO_SUPPRESS =
+      ImmutableList.of(
+          com.jetbrains.python.run.PythonRunConfigurationProducer.class,
+          com.jetbrains.python.testing.universalTests.PyUniversalTestsConfigurationProducer.class,
+          com.jetbrains.python.testing.nosetest.PythonNoseTestConfigurationProducer.class,
+          com.jetbrains.python.testing.doctest.PythonDocTestConfigurationProducer.class,
+          com.jetbrains.python.testing.pytest.PyTestConfigurationProducer.class,
+          com.jetbrains.python.testing.unittest.PythonUnitTestConfigurationProducer.class);
+}
diff --git a/sdkcompat/v171/com/google/idea/sdkcompat/python/PyImportResolverAdapter.java b/sdkcompat/v171/com/google/idea/sdkcompat/python/PyImportResolverAdapter.java
new file mode 100644
index 0000000..7edba6c
--- /dev/null
+++ b/sdkcompat/v171/com/google/idea/sdkcompat/python/PyImportResolverAdapter.java
@@ -0,0 +1,23 @@
+package com.google.idea.sdkcompat.python;
+
+import com.intellij.psi.PsiElement;
+import com.intellij.psi.util.QualifiedName;
+import com.jetbrains.python.psi.impl.PyImportResolver;
+import com.jetbrains.python.psi.resolve.PyQualifiedNameResolveContext;
+import javax.annotation.Nullable;
+
+/** Adapter to bridge different SDK versions. */
+public interface PyImportResolverAdapter extends PyImportResolver {
+
+  @Nullable
+  PsiElement resolveImportReference(
+      QualifiedName name, PyQualifiedNameResolveContextAdapter context, boolean withRoots);
+
+  @Override
+  @Nullable
+  default PsiElement resolveImportReference(
+      QualifiedName name, PyQualifiedNameResolveContext context, boolean withRoots) {
+    return resolveImportReference(
+        name, new PyQualifiedNameResolveContextAdapter(context), withRoots);
+  }
+}
diff --git a/sdkcompat/v171/com/google/idea/sdkcompat/python/PyQualifiedNameResolveContextAdapter.java b/sdkcompat/v171/com/google/idea/sdkcompat/python/PyQualifiedNameResolveContextAdapter.java
new file mode 100644
index 0000000..7311580
--- /dev/null
+++ b/sdkcompat/v171/com/google/idea/sdkcompat/python/PyQualifiedNameResolveContextAdapter.java
@@ -0,0 +1,152 @@
+package com.google.idea.sdkcompat.python;
+
+import com.intellij.openapi.module.Module;
+import com.intellij.openapi.project.Project;
+import com.intellij.openapi.projectRoots.Sdk;
+import com.intellij.psi.PsiDirectory;
+import com.intellij.psi.PsiElement;
+import com.intellij.psi.PsiFile;
+import com.intellij.psi.PsiManager;
+import com.jetbrains.python.psi.resolve.PyQualifiedNameResolveContext;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+/** Adapter to bridge different SDK versions. */
+public class PyQualifiedNameResolveContextAdapter implements PyQualifiedNameResolveContext {
+
+  private final PyQualifiedNameResolveContext delegate;
+
+  PyQualifiedNameResolveContextAdapter(PyQualifiedNameResolveContext delegate) {
+    this.delegate = delegate;
+  }
+
+  @Nullable
+  @Override
+  public PsiElement getFoothold() {
+    return delegate.getFoothold();
+  }
+
+  @Override
+  public int getRelativeLevel() {
+    return delegate.getRelativeLevel();
+  }
+
+  @Nullable
+  @Override
+  public Sdk getSdk() {
+    return delegate.getSdk();
+  }
+
+  @Nullable
+  @Override
+  public Module getModule() {
+    return delegate.getModule();
+  }
+
+  @NotNull
+  @Override
+  public Project getProject() {
+    return delegate.getProject();
+  }
+
+  @Override
+  public boolean getWithoutRoots() {
+    return delegate.getWithoutRoots();
+  }
+
+  @Override
+  public boolean getWithoutForeign() {
+    return delegate.getWithoutForeign();
+  }
+
+  @Override
+  public boolean getWithoutStubs() {
+    return delegate.getWithoutStubs();
+  }
+
+  @NotNull
+  @Override
+  public PsiManager getPsiManager() {
+    return delegate.getPsiManager();
+  }
+
+  @Override
+  public boolean getWithMembers() {
+    return delegate.getWithMembers();
+  }
+
+  @Override
+  public boolean getWithPlainDirectories() {
+    return delegate.getWithPlainDirectories();
+  }
+
+  @Override
+  public boolean getVisitAllModules() {
+    return delegate.getVisitAllModules();
+  }
+
+  @Nullable
+  @Override
+  public Sdk getEffectiveSdk() {
+    return delegate.getEffectiveSdk();
+  }
+
+  @Override
+  public boolean isValid() {
+    return delegate.isValid();
+  }
+
+  @Nullable
+  @Override
+  public PsiFile getFootholdFile() {
+    return delegate.getFootholdFile();
+  }
+
+  @Nullable
+  @Override
+  public PsiDirectory getContainingDirectory() {
+    return delegate.getContainingDirectory();
+  }
+
+  @NotNull
+  @Override
+  public PyQualifiedNameResolveContext copyWithoutForeign() {
+    return delegate.copyWithoutForeign();
+  }
+
+  @NotNull
+  @Override
+  public PyQualifiedNameResolveContext copyWithMembers() {
+    return delegate.copyWithMembers();
+  }
+
+  @NotNull
+  @Override
+  public PyQualifiedNameResolveContext copyWithPlainDirectories() {
+    return delegate.copyWithPlainDirectories();
+  }
+
+  @NotNull
+  @Override
+  public PyQualifiedNameResolveContext copyWithRelative(int i) {
+    return delegate.copyWithRelative(i);
+  }
+
+  @NotNull
+  @Override
+  public PyQualifiedNameResolveContext copyWithoutRoots() {
+    return delegate.copyWithoutRoots();
+  }
+
+  @NotNull
+  @Override
+  public PyQualifiedNameResolveContext copyWithRoots() {
+    return delegate.copyWithRoots();
+  }
+
+  @NotNull
+  @Override
+  public PyQualifiedNameResolveContext copyWithoutStubs() {
+    return delegate.copyWithoutStubs();
+  }
+}
diff --git a/sdkcompat/v171/com/google/idea/sdkcompat/python/PyReferenceResolveProviderAdapter.java b/sdkcompat/v171/com/google/idea/sdkcompat/python/PyReferenceResolveProviderAdapter.java
new file mode 100644
index 0000000..9f8b5c3
--- /dev/null
+++ b/sdkcompat/v171/com/google/idea/sdkcompat/python/PyReferenceResolveProviderAdapter.java
@@ -0,0 +1,6 @@
+package com.google.idea.sdkcompat.python;
+
+import com.jetbrains.python.psi.resolve.PyReferenceResolveProvider;
+
+/** Adapter to bridge different SDK versions. */
+public interface PyReferenceResolveProviderAdapter extends PyReferenceResolveProvider {}
diff --git a/sdkcompat/v171/com/google/idea/sdkcompat/python/ResolveImportCompatUtils.java b/sdkcompat/v171/com/google/idea/sdkcompat/python/ResolveImportCompatUtils.java
new file mode 100644
index 0000000..0329801
--- /dev/null
+++ b/sdkcompat/v171/com/google/idea/sdkcompat/python/ResolveImportCompatUtils.java
@@ -0,0 +1,22 @@
+package com.google.idea.sdkcompat.python;
+
+import com.intellij.psi.PsiElement;
+import com.intellij.psi.PsiFile;
+import com.jetbrains.python.psi.resolve.ResolveImportUtil;
+import javax.annotation.Nullable;
+
+/** Handles changes to {@link ResolveImportUtil} between our supported versions. */
+public class ResolveImportCompatUtils {
+
+  @Nullable
+  public static PsiElement resolveChild(
+      @Nullable PsiElement parent,
+      String referencedName,
+      @Nullable PsiFile containingFile,
+      boolean fileOnly,
+      boolean checkForPackage,
+      boolean withoutStubs) {
+    return ResolveImportUtil.resolveChild(
+        parent, referencedName, containingFile, fileOnly, checkForPackage, withoutStubs);
+  }
+}
diff --git a/sdkcompat/v145/com/google/idea/sdkcompat/smrunner/SmRunnerCompatUtils.java b/sdkcompat/v171/com/google/idea/sdkcompat/smrunner/SmRunnerCompatUtils.java
similarity index 77%
rename from sdkcompat/v145/com/google/idea/sdkcompat/smrunner/SmRunnerCompatUtils.java
rename to sdkcompat/v171/com/google/idea/sdkcompat/smrunner/SmRunnerCompatUtils.java
index c4bd9a7..8555ad8 100644
--- a/sdkcompat/v145/com/google/idea/sdkcompat/smrunner/SmRunnerCompatUtils.java
+++ b/sdkcompat/v171/com/google/idea/sdkcompat/smrunner/SmRunnerCompatUtils.java
@@ -8,6 +8,7 @@
 
   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);
+    return new TestFailedEvent(
+        name, null, message, content, true, null, null, null, null, false, false, duration);
   }
 }
diff --git a/sdkcompat/v171/com/google/idea/sdkcompat/transactions/Transactions.java b/sdkcompat/v171/com/google/idea/sdkcompat/transactions/Transactions.java
new file mode 100644
index 0000000..8862aa8
--- /dev/null
+++ b/sdkcompat/v171/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/v171/com/google/idea/sdkcompat/vcs/ChangeListManagerSdkCompatAdapter.java b/sdkcompat/v171/com/google/idea/sdkcompat/vcs/ChangeListManagerSdkCompatAdapter.java
new file mode 100644
index 0000000..1e525c1
--- /dev/null
+++ b/sdkcompat/v171/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/sdkcompat/v171/com/google/idea/sdkcompat/vcs/MergeDataBuilder.java b/sdkcompat/v171/com/google/idea/sdkcompat/vcs/MergeDataBuilder.java
new file mode 100644
index 0000000..49b4665
--- /dev/null
+++ b/sdkcompat/v171/com/google/idea/sdkcompat/vcs/MergeDataBuilder.java
@@ -0,0 +1,78 @@
+package com.google.idea.sdkcompat.vcs;
+
+import com.intellij.openapi.vcs.FilePath;
+import com.intellij.openapi.vcs.history.VcsRevisionNumber;
+import com.intellij.openapi.vcs.merge.MergeData;
+import org.jetbrains.annotations.Nullable;
+
+/** SDK adapter for creating {@link MergeData}. */
+// TODO(grl): Move to com.google.devtools.intellij.piper.resolve and make package-private
+// once versions less than v171 have been deleted. We may as well keep the builder around,
+// since it uses piper-relevant terminology and complies with Java style conventions.
+public final class MergeDataBuilder {
+  private byte[] baseContent;
+  private byte[] theirsContent;
+  private byte[] yoursContent;
+
+  @Nullable private VcsRevisionNumber baseRevisionNumber;
+  @Nullable private VcsRevisionNumber theirsRevisionNumber;
+  @Nullable private VcsRevisionNumber yoursRevisionNumber;
+
+  @Nullable private FilePath baseFilePath;
+  @Nullable private FilePath theirsFilePath;
+  @Nullable private FilePath yoursFilePath;
+
+  public void setBaseContent(byte[] baseContent) {
+    this.baseContent = baseContent;
+  }
+
+  public void setTheirsContent(byte[] theirsContent) {
+    this.theirsContent = theirsContent;
+  }
+
+  public void setYoursContent(byte[] yoursContent) {
+    this.yoursContent = yoursContent;
+  }
+
+  public void setBaseRevisionNumber(@Nullable VcsRevisionNumber baseRevisionNumber) {
+    this.baseRevisionNumber = baseRevisionNumber;
+  }
+
+  public void setTheirsRevisionNumber(@Nullable VcsRevisionNumber theirsRevisionNumber) {
+    this.theirsRevisionNumber = theirsRevisionNumber;
+  }
+
+  public void setYoursRevisionNumber(@Nullable VcsRevisionNumber yoursRevisionNumber) {
+    this.yoursRevisionNumber = yoursRevisionNumber;
+  }
+
+  public void setBaseFilePath(@Nullable FilePath baseFilePath) {
+    this.baseFilePath = baseFilePath;
+  }
+
+  public void setTheirsFilePath(@Nullable FilePath theirsFilePath) {
+    this.theirsFilePath = theirsFilePath;
+  }
+
+  public void setYoursFilePath(@Nullable FilePath yoursFilePath) {
+    this.yoursFilePath = yoursFilePath;
+  }
+
+  public MergeData build() {
+    MergeData mergeData = new MergeData();
+
+    mergeData.ORIGINAL = baseContent;
+    mergeData.ORIGINAL_REVISION_NUMBER = baseRevisionNumber;
+    mergeData.ORIGINAL_FILE_PATH = baseFilePath;
+
+    mergeData.LAST = theirsContent;
+    mergeData.LAST_REVISION_NUMBER = theirsRevisionNumber;
+    mergeData.LAST_FILE_PATH = theirsFilePath;
+
+    mergeData.CURRENT = yoursContent;
+    mergeData.CURRENT_REVISION_NUMBER = yoursRevisionNumber;
+    mergeData.CURRENT_FILE_PATH = yoursFilePath;
+
+    return mergeData;
+  }
+}
diff --git a/terminal/BUILD b/terminal/BUILD
new file mode 100644
index 0000000..d3f2562
--- /dev/null
+++ b/terminal/BUILD
@@ -0,0 +1,22 @@
+licenses(["notice"])  # Apache 2.0
+
+load("//build_defs:build_defs.bzl", "optional_plugin_xml")
+
+java_library(
+    name = "terminal",
+    srcs = glob(["src/**/*.java"]),
+    visibility = ["//visibility:public"],
+    deps = [
+        "//base",
+        "//intellij_platform_sdk:plugin_api",
+        "//intellij_platform_sdk:terminal",
+        "@jsr305_annotations//jar",
+    ],
+)
+
+optional_plugin_xml(
+    name = "optional_xml",
+    module = "org.jetbrains.plugins.terminal",
+    plugin_xml = "src/META-INF/terminal-contents.xml",
+    visibility = ["//visibility:public"],
+)
diff --git a/terminal/src/META-INF/terminal-contents.xml b/terminal/src/META-INF/terminal-contents.xml
new file mode 100644
index 0000000..b5b3255
--- /dev/null
+++ b/terminal/src/META-INF/terminal-contents.xml
@@ -0,0 +1,5 @@
+<idea-plugin>
+  <extensions defaultExtensionNs="org.jetbrains.plugins.terminal">
+    <localTerminalCustomizer implementation="com.google.idea.blaze.terminal.DefaultTerminalLocationCustomizer"/>
+  </extensions>
+</idea-plugin>
diff --git a/terminal/src/com/google/idea/blaze/terminal/DefaultTerminalLocationCustomizer.java b/terminal/src/com/google/idea/blaze/terminal/DefaultTerminalLocationCustomizer.java
new file mode 100644
index 0000000..8023f0a
--- /dev/null
+++ b/terminal/src/com/google/idea/blaze/terminal/DefaultTerminalLocationCustomizer.java
@@ -0,0 +1,33 @@
+/*
+ * Copyright 2017 The Bazel Authors. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.idea.blaze.terminal;
+
+import com.google.idea.blaze.base.model.primitives.WorkspaceRoot;
+import com.intellij.openapi.project.Project;
+import javax.annotation.Nullable;
+import org.jetbrains.plugins.terminal.LocalTerminalCustomizer;
+
+/** Set the default terminal path to the workspace root. */
+public class DefaultTerminalLocationCustomizer extends LocalTerminalCustomizer {
+
+  // added in 2017.1, so foregoing the override annotation for backwards compatibility.
+  @SuppressWarnings("Overrides")
+  @Nullable
+  protected String getDefaultFolder(Project project) {
+    WorkspaceRoot root = WorkspaceRoot.fromProjectSafe(project);
+    return root != null ? root.toString() : null;
+  }
+}
diff --git a/testing/BUILD b/testing/BUILD
index 8b2600e..e9a3c7a 100644
--- a/testing/BUILD
+++ b/testing/BUILD
@@ -6,13 +6,24 @@
 
 licenses(["notice"])  # Apache 2.0
 
+load("//intellij_platform_sdk:build_defs.bzl", "select_for_ide")
+
 java_library(
     name = "lib",
     testonly = 1,
-    srcs = glob(["src/**/*.java"]),
+    srcs = glob(["src/**/*.java"]) +
+           select_for_ide(
+               android_studio = glob(["cidr/src/**/*.java"]),
+               clion = glob(["cidr/src/**/*.java"]),
+               default = [],
+           ),
     deps = [
         "//intellij_platform_sdk:plugin_api_for_tests",
         "@jsr305_annotations//jar",
         "@junit//jar",
-    ],
+    ] + select_for_ide(
+        android_studio = ["//sdkcompat"],
+        clion = ["//sdkcompat"],
+        default = [],
+    ),
 )
diff --git a/testing/cidr/src/com/google/idea/testing/cidr/StubOCCompilerMacros.java b/testing/cidr/src/com/google/idea/testing/cidr/StubOCCompilerMacros.java
new file mode 100644
index 0000000..6345104
--- /dev/null
+++ b/testing/cidr/src/com/google/idea/testing/cidr/StubOCCompilerMacros.java
@@ -0,0 +1,12 @@
+package com.google.idea.testing.cidr;
+
+import com.intellij.psi.PsiFile;
+import com.jetbrains.cidr.lang.preprocessor.OCInclusionContext;
+import com.jetbrains.cidr.lang.workspace.compiler.OCCompilerMacros;
+
+/** Stub {@link OCCompilerMacros} for testing. */
+class StubOCCompilerMacros extends OCCompilerMacros {
+
+  @Override
+  protected void fillFileMacros(OCInclusionContext context, PsiFile sourceFile) {}
+}
diff --git a/testing/cidr/src/com/google/idea/testing/cidr/StubOCCompilerSettings.java b/testing/cidr/src/com/google/idea/testing/cidr/StubOCCompilerSettings.java
new file mode 100644
index 0000000..41e0dee
--- /dev/null
+++ b/testing/cidr/src/com/google/idea/testing/cidr/StubOCCompilerSettings.java
@@ -0,0 +1,52 @@
+package com.google.idea.testing.cidr;
+
+import com.intellij.openapi.project.Project;
+import com.intellij.openapi.vfs.VfsUtilCore;
+import com.intellij.openapi.vfs.VirtualFile;
+import com.jetbrains.cidr.lang.OCLanguageKind;
+import com.jetbrains.cidr.lang.toolchains.CidrCompilerSwitches;
+import com.jetbrains.cidr.lang.toolchains.CidrSwitchBuilder;
+import com.jetbrains.cidr.lang.toolchains.CidrToolEnvironment;
+import com.jetbrains.cidr.lang.toolchains.DefaultCidrToolEnvironment;
+import com.jetbrains.cidr.lang.workspace.compiler.OCCompilerKind;
+import com.jetbrains.cidr.lang.workspace.compiler.OCCompilerSettings;
+import java.io.File;
+import javax.annotation.Nullable;
+
+/** Stub {@link OCCompilerSettings} for testing. */
+class StubOCCompilerSettings extends OCCompilerSettings {
+
+  private final Project project;
+
+  StubOCCompilerSettings(Project project) {
+    this.project = project;
+  }
+
+  @Nullable
+  @Override
+  public OCCompilerKind getCompiler(OCLanguageKind languageKind) {
+    return OCCompilerKind.CLANG;
+  }
+
+  @Nullable
+  @Override
+  public File getCompilerExecutable(OCLanguageKind languageKind) {
+    return null;
+  }
+
+  @Override
+  public File getCompilerWorkingDir() {
+    return VfsUtilCore.virtualToIoFile(project.getBaseDir());
+  }
+
+  @Override
+  public CidrToolEnvironment getEnvironment() {
+    return new DefaultCidrToolEnvironment();
+  }
+
+  @Override
+  public CidrCompilerSwitches getCompilerSwitches(
+      OCLanguageKind languageKind, @Nullable VirtualFile sourceFile) {
+    return new CidrSwitchBuilder().build();
+  }
+}
diff --git a/testing/cidr/src/com/google/idea/testing/cidr/StubOCResolveConfiguration.java b/testing/cidr/src/com/google/idea/testing/cidr/StubOCResolveConfiguration.java
new file mode 100644
index 0000000..6d6c083
--- /dev/null
+++ b/testing/cidr/src/com/google/idea/testing/cidr/StubOCResolveConfiguration.java
@@ -0,0 +1,104 @@
+package com.google.idea.testing.cidr;
+
+import com.google.common.collect.ImmutableList;
+import com.intellij.openapi.project.Project;
+import com.intellij.openapi.util.UserDataHolderBase;
+import com.intellij.openapi.vfs.VirtualFile;
+import com.jetbrains.cidr.lang.OCFileTypeHelpers;
+import com.jetbrains.cidr.lang.OCLanguageKind;
+import com.jetbrains.cidr.lang.workspace.OCLanguageKindCalculator;
+import com.jetbrains.cidr.lang.workspace.OCResolveConfiguration;
+import com.jetbrains.cidr.lang.workspace.OCResolveRootAndConfiguration;
+import com.jetbrains.cidr.lang.workspace.OCWorkspaceUtil;
+import com.jetbrains.cidr.lang.workspace.compiler.OCCompilerMacros;
+import com.jetbrains.cidr.lang.workspace.compiler.OCCompilerSettings;
+import com.jetbrains.cidr.lang.workspace.headerRoots.HeaderRoots;
+import javax.annotation.Nullable;
+
+/** Stub {@link OCResolveConfiguration} for testing. */
+class StubOCResolveConfiguration extends UserDataHolderBase implements OCResolveConfiguration {
+
+  private final Project project;
+  private final HeaderRoots projectIncludeRoots;
+  private final OCCompilerSettings compilerSettings;
+  private final OCCompilerMacros compilerMacros;
+
+  StubOCResolveConfiguration(Project project) {
+    this.project = project;
+    this.projectIncludeRoots = new HeaderRoots(ImmutableList.of());
+    this.compilerMacros = new StubOCCompilerMacros();
+    this.compilerSettings = new StubOCCompilerSettings(project);
+  }
+
+  @Override
+  public Project getProject() {
+    return project;
+  }
+
+  @Override
+  public String getDisplayName(boolean shorten) {
+    return "Stub resolve configuration";
+  }
+
+  @Nullable
+  @Override
+  public VirtualFile getPrecompiledHeader() {
+    return null;
+  }
+
+  @Nullable
+  @Override
+  public OCLanguageKind getDeclaredLanguageKind(VirtualFile sourceOrHeaderFile) {
+    String fileName = sourceOrHeaderFile.getName();
+    if (OCFileTypeHelpers.isSourceFile(fileName)) {
+      return getLanguageKind(sourceOrHeaderFile);
+    }
+    return getMaximumLanguageKind();
+  }
+
+  private OCLanguageKind getLanguageKind(VirtualFile sourceFile) {
+    OCLanguageKind kind = OCLanguageKindCalculator.tryFileTypeAndExtension(project, sourceFile);
+    return kind != null ? kind : getMaximumLanguageKind();
+  }
+
+  @Override
+  public OCLanguageKind getMaximumLanguageKind() {
+    return OCLanguageKind.CPP;
+  }
+
+  @Override
+  public OCLanguageKind getPrecompiledLanguageKind() {
+    return getMaximumLanguageKind();
+  }
+
+  @Override
+  public HeaderRoots getProjectHeadersRoots() {
+    return projectIncludeRoots;
+  }
+
+  @Override
+  public HeaderRoots getLibraryHeadersRoots(OCResolveRootAndConfiguration headerContext) {
+    return projectIncludeRoots;
+  }
+
+  @Override
+  public OCCompilerMacros getCompilerMacros() {
+    return compilerMacros;
+  }
+
+  @Override
+  public OCCompilerSettings getCompilerSettings() {
+    return compilerSettings;
+  }
+
+  @Nullable
+  @Override
+  public Object getIndexingCluster() {
+    return null;
+  }
+
+  @Override
+  public int compareTo(OCResolveConfiguration o) {
+    return OCWorkspaceUtil.compareConfigurations(this, o);
+  }
+}
diff --git a/testing/cidr/src/com/google/idea/testing/cidr/StubOCWorkspace.java b/testing/cidr/src/com/google/idea/testing/cidr/StubOCWorkspace.java
new file mode 100644
index 0000000..1f58463
--- /dev/null
+++ b/testing/cidr/src/com/google/idea/testing/cidr/StubOCWorkspace.java
@@ -0,0 +1,68 @@
+package com.google.idea.testing.cidr;
+
+import com.google.common.collect.ImmutableList;
+import com.google.idea.sdkcompat.cidr.OCWorkspaceAdapter;
+import com.intellij.openapi.project.Project;
+import com.intellij.openapi.vfs.VirtualFile;
+import com.jetbrains.cidr.lang.OCFileType;
+import com.jetbrains.cidr.lang.symbols.OCSymbol;
+import com.jetbrains.cidr.lang.workspace.OCResolveConfiguration;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+import java.util.Objects;
+import javax.annotation.Nullable;
+
+/** A stub {@link com.jetbrains.cidr.lang.workspace.OCWorkspace} to use for testing. */
+class StubOCWorkspace extends OCWorkspaceAdapter {
+
+  private final List<OCResolveConfiguration> resolveConfigurations;
+
+  StubOCWorkspace(Project project) {
+    super(project);
+    resolveConfigurations = new ArrayList<>();
+    resolveConfigurations.add(new StubOCResolveConfiguration(project));
+  }
+
+  @Override
+  public Collection<VirtualFile> getLibraryFilesToBuildSymbols() {
+    return ImmutableList.of();
+  }
+
+  @Override
+  public boolean areFromSameProject(@Nullable VirtualFile a, @Nullable VirtualFile b) {
+    return Objects.equals(a, b);
+  }
+
+  @Override
+  public boolean areFromSamePackage(@Nullable VirtualFile a, @Nullable VirtualFile b) {
+    return Objects.equals(a, b);
+  }
+
+  @Override
+  public boolean isInSDK(@Nullable VirtualFile file) {
+    return false;
+  }
+
+  @Override
+  public boolean isFromWrongSDK(OCSymbol symbol, @Nullable VirtualFile contextFile) {
+    return false;
+  }
+
+  @Override
+  public List<? extends OCResolveConfiguration> getConfigurations() {
+    return resolveConfigurations;
+  }
+
+  @Override
+  public List<? extends OCResolveConfiguration> getConfigurationsForFile(
+      @Nullable VirtualFile sourceFile) {
+    if (sourceFile == null) {
+      return Collections.emptyList();
+    }
+    return OCFileType.INSTANCE.equals(sourceFile.getFileType())
+        ? resolveConfigurations
+        : Collections.emptyList();
+  }
+}
diff --git a/testing/cidr/src/com/google/idea/testing/cidr/StubOCWorkspaceManager.java b/testing/cidr/src/com/google/idea/testing/cidr/StubOCWorkspaceManager.java
new file mode 100644
index 0000000..05921b7
--- /dev/null
+++ b/testing/cidr/src/com/google/idea/testing/cidr/StubOCWorkspaceManager.java
@@ -0,0 +1,90 @@
+package com.google.idea.testing.cidr;
+
+import com.intellij.lang.Language;
+import com.intellij.openapi.application.ApplicationManager;
+import com.intellij.openapi.fileTypes.PlainTextLanguage;
+import com.intellij.openapi.project.Project;
+import com.intellij.openapi.vfs.VirtualFile;
+import com.intellij.psi.PsiFile;
+import com.intellij.psi.PsiManager;
+import com.intellij.psi.impl.PsiManagerEx;
+import com.intellij.psi.impl.file.impl.FileManager;
+import com.intellij.util.ui.UIUtil;
+import com.jetbrains.cidr.lang.OCLanguage;
+import com.jetbrains.cidr.lang.preprocessor.OCInclusionContextUtil;
+import com.jetbrains.cidr.lang.workspace.OCWorkspace;
+import com.jetbrains.cidr.lang.workspace.OCWorkspaceManager;
+
+/**
+ * A stub {@link OCWorkspaceManager} to use for testing. Also allows toggling on C++ support (which
+ * may have been disabled by other OCWorkspaceManagers.
+ *
+ * <p>Once the plugin API ships with a more official OCWorkspaceManager-for-testing, we may be able
+ * to switch over to those classes. See: b/32420569
+ */
+public class StubOCWorkspaceManager extends OCWorkspaceManager {
+
+  private final Project project;
+  private final OCWorkspace workspace;
+
+  public StubOCWorkspaceManager(Project project) {
+    this.project = project;
+    this.workspace = new StubOCWorkspace(project);
+  }
+
+  @Override
+  public OCWorkspace getWorkspace() {
+    return workspace;
+  }
+
+  /**
+   * Enable C++ language support for testing (a previously registered OCWorkspace which may have
+   * disabled language support).
+   */
+  public void enableCSupportForTesting() throws Exception {
+    OCWorkspace workspace = OCWorkspaceManager.getWorkspace(project);
+    Boolean isCurrentlyEnabled = !OCLanguage.LANGUAGE_SUPPORT_DISABLED.get(project, false);
+    if (!isCurrentlyEnabled) {
+      enableLanguageSupport(project);
+      rebuildSymbols(project, workspace);
+    }
+  }
+
+  private static void enableLanguageSupport(Project project) {
+    OCLanguage.LANGUAGE_SUPPORT_DISABLED.set(project, false);
+    UIUtil.invokeLaterIfNeeded(
+        () ->
+            ApplicationManager.getApplication()
+                .runWriteAction(
+                    () -> {
+                      if (project.isDisposed()) {
+                        return;
+                      }
+                      Language langToReset = PlainTextLanguage.INSTANCE;
+                      FileManager fileManager =
+                          ((PsiManagerEx) PsiManager.getInstance(project)).getFileManager();
+                      for (PsiFile file : fileManager.getAllCachedFiles()) {
+                        if (file.getLanguage() == langToReset) {
+                          VirtualFile vf = OCInclusionContextUtil.getVirtualFile(file);
+                          if (vf != null) {
+                            fileManager.setViewProvider(vf, null);
+                          }
+                        }
+                      }
+                    }));
+  }
+
+  private static void rebuildSymbols(Project project, OCWorkspace workspace) {
+    ApplicationManager.getApplication()
+        .runReadAction(
+            () -> {
+              if (project.isDisposed()) {
+                return;
+              }
+              workspace
+                  .getModificationTrackers()
+                  .getBuildSettingsChangesTracker()
+                  .incModificationCount();
+            });
+  }
+}
diff --git a/third_party/BUILD b/third_party/BUILD
index 283fe42..06e9b19 100644
--- a/third_party/BUILD
+++ b/third_party/BUILD
@@ -10,3 +10,13 @@
     name = "zip",
     srcs = ["zip-wrap/zip.sh"],
 )
+
+sh_binary(
+    name = "unzip",
+    srcs = ["zip-wrap/unzip.sh"],
+)
+
+java_library(
+    name = "python",
+    exports = ["//third_party/python"],
+)
diff --git a/third_party/python/BUILD b/third_party/python/BUILD
new file mode 100644
index 0000000..2f69d16
--- /dev/null
+++ b/third_party/python/BUILD
@@ -0,0 +1,27 @@
+package(default_visibility = ["//visibility:public"])
+
+licenses(["notice"])
+
+load("//intellij_platform_sdk:build_defs.bzl", "select_for_plugin_api")
+
+java_library(
+    name = "python_internal",
+    visibility = ["//visibility:private"],
+    exports = select_for_plugin_api({
+        "intellij-2016.3.1": ["@python_2016_3//:python"],
+        "intellij-2017.1.1": ["@python_2017_1//:python"],
+        "clion-2017.1.1": ["@clion_2017_1_1//:python"],
+    }),
+)
+
+java_library(
+    name = "python_for_tests",
+    testonly = 1,
+    exports = [":python_internal"],
+)
+
+java_library(
+    name = "python",
+    neverlink = 1,
+    exports = [":python_internal"],
+)
diff --git a/third_party/python/LICENSE b/third_party/python/LICENSE
new file mode 100644
index 0000000..d645695
--- /dev/null
+++ b/third_party/python/LICENSE
@@ -0,0 +1,202 @@
+
+                                 Apache License
+                           Version 2.0, January 2004
+                        http://www.apache.org/licenses/
+
+   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+   1. Definitions.
+
+      "License" shall mean the terms and conditions for use, reproduction,
+      and distribution as defined by Sections 1 through 9 of this document.
+
+      "Licensor" shall mean the copyright owner or entity authorized by
+      the copyright owner that is granting the License.
+
+      "Legal Entity" shall mean the union of the acting entity and all
+      other entities that control, are controlled by, or are under common
+      control with that entity. For the purposes of this definition,
+      "control" means (i) the power, direct or indirect, to cause the
+      direction or management of such entity, whether by contract or
+      otherwise, or (ii) ownership of fifty percent (50%) or more of the
+      outstanding shares, or (iii) beneficial ownership of such entity.
+
+      "You" (or "Your") shall mean an individual or Legal Entity
+      exercising permissions granted by this License.
+
+      "Source" form shall mean the preferred form for making modifications,
+      including but not limited to software source code, documentation
+      source, and configuration files.
+
+      "Object" form shall mean any form resulting from mechanical
+      transformation or translation of a Source form, including but
+      not limited to compiled object code, generated documentation,
+      and conversions to other media types.
+
+      "Work" shall mean the work of authorship, whether in Source or
+      Object form, made available under the License, as indicated by a
+      copyright notice that is included in or attached to the work
+      (an example is provided in the Appendix below).
+
+      "Derivative Works" shall mean any work, whether in Source or Object
+      form, that is based on (or derived from) the Work and for which the
+      editorial revisions, annotations, elaborations, or other modifications
+      represent, as a whole, an original work of authorship. For the purposes
+      of this License, Derivative Works shall not include works that remain
+      separable from, or merely link (or bind by name) to the interfaces of,
+      the Work and Derivative Works thereof.
+
+      "Contribution" shall mean any work of authorship, including
+      the original version of the Work and any modifications or additions
+      to that Work or Derivative Works thereof, that is intentionally
+      submitted to Licensor for inclusion in the Work by the copyright owner
+      or by an individual or Legal Entity authorized to submit on behalf of
+      the copyright owner. For the purposes of this definition, "submitted"
+      means any form of electronic, verbal, or written communication sent
+      to the Licensor or its representatives, including but not limited to
+      communication on electronic mailing lists, source code control systems,
+      and issue tracking systems that are managed by, or on behalf of, the
+      Licensor for the purpose of discussing and improving the Work, but
+      excluding communication that is conspicuously marked or otherwise
+      designated in writing by the copyright owner as "Not a Contribution."
+
+      "Contributor" shall mean Licensor and any individual or Legal Entity
+      on behalf of whom a Contribution has been received by Licensor and
+      subsequently incorporated within the Work.
+
+   2. Grant of Copyright License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      copyright license to reproduce, prepare Derivative Works of,
+      publicly display, publicly perform, sublicense, and distribute the
+      Work and such Derivative Works in Source or Object form.
+
+   3. Grant of Patent License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      (except as stated in this section) patent license to make, have made,
+      use, offer to sell, sell, import, and otherwise transfer the Work,
+      where such license applies only to those patent claims licensable
+      by such Contributor that are necessarily infringed by their
+      Contribution(s) alone or by combination of their Contribution(s)
+      with the Work to which such Contribution(s) was submitted. If You
+      institute patent litigation against any entity (including a
+      cross-claim or counterclaim in a lawsuit) alleging that the Work
+      or a Contribution incorporated within the Work constitutes direct
+      or contributory patent infringement, then any patent licenses
+      granted to You under this License for that Work shall terminate
+      as of the date such litigation is filed.
+
+   4. Redistribution. You may reproduce and distribute copies of the
+      Work or Derivative Works thereof in any medium, with or without
+      modifications, and in Source or Object form, provided that You
+      meet the following conditions:
+
+      (a) You must give any other recipients of the Work or
+          Derivative Works a copy of this License; and
+
+      (b) You must cause any modified files to carry prominent notices
+          stating that You changed the files; and
+
+      (c) You must retain, in the Source form of any Derivative Works
+          that You distribute, all copyright, patent, trademark, and
+          attribution notices from the Source form of the Work,
+          excluding those notices that do not pertain to any part of
+          the Derivative Works; and
+
+      (d) If the Work includes a "NOTICE" text file as part of its
+          distribution, then any Derivative Works that You distribute must
+          include a readable copy of the attribution notices contained
+          within such NOTICE file, excluding those notices that do not
+          pertain to any part of the Derivative Works, in at least one
+          of the following places: within a NOTICE text file distributed
+          as part of the Derivative Works; within the Source form or
+          documentation, if provided along with the Derivative Works; or,
+          within a display generated by the Derivative Works, if and
+          wherever such third-party notices normally appear. The contents
+          of the NOTICE file are for informational purposes only and
+          do not modify the License. You may add Your own attribution
+          notices within Derivative Works that You distribute, alongside
+          or as an addendum to the NOTICE text from the Work, provided
+          that such additional attribution notices cannot be construed
+          as modifying the License.
+
+      You may add Your own copyright statement to Your modifications and
+      may provide additional or different license terms and conditions
+      for use, reproduction, or distribution of Your modifications, or
+      for any such Derivative Works as a whole, provided Your use,
+      reproduction, and distribution of the Work otherwise complies with
+      the conditions stated in this License.
+
+   5. Submission of Contributions. Unless You explicitly state otherwise,
+      any Contribution intentionally submitted for inclusion in the Work
+      by You to the Licensor shall be under the terms and conditions of
+      this License, without any additional terms or conditions.
+      Notwithstanding the above, nothing herein shall supersede or modify
+      the terms of any separate license agreement you may have executed
+      with Licensor regarding such Contributions.
+
+   6. Trademarks. This License does not grant permission to use the trade
+      names, trademarks, service marks, or product names of the Licensor,
+      except as required for reasonable and customary use in describing the
+      origin of the Work and reproducing the content of the NOTICE file.
+
+   7. Disclaimer of Warranty. Unless required by applicable law or
+      agreed to in writing, Licensor provides the Work (and each
+      Contributor provides its Contributions) on an "AS IS" BASIS,
+      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+      implied, including, without limitation, any warranties or conditions
+      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+      PARTICULAR PURPOSE. You are solely responsible for determining the
+      appropriateness of using or redistributing the Work and assume any
+      risks associated with Your exercise of permissions under this License.
+
+   8. Limitation of Liability. In no event and under no legal theory,
+      whether in tort (including negligence), contract, or otherwise,
+      unless required by applicable law (such as deliberate and grossly
+      negligent acts) or agreed to in writing, shall any Contributor be
+      liable to You for damages, including any direct, indirect, special,
+      incidental, or consequential damages of any character arising as a
+      result of this License or out of the use or inability to use the
+      Work (including but not limited to damages for loss of goodwill,
+      work stoppage, computer failure or malfunction, or any and all
+      other commercial damages or losses), even if such Contributor
+      has been advised of the possibility of such damages.
+
+   9. Accepting Warranty or Additional Liability. While redistributing
+      the Work or Derivative Works thereof, You may choose to offer,
+      and charge a fee for, acceptance of support, warranty, indemnity,
+      or other liability obligations and/or rights consistent with this
+      License. However, in accepting such obligations, You may act only
+      on Your own behalf and on Your sole responsibility, not on behalf
+      of any other Contributor, and only if You agree to indemnify,
+      defend, and hold each Contributor harmless for any liability
+      incurred by, or claims asserted against, such Contributor by reason
+      of your accepting any such warranty or additional liability.
+
+   END OF TERMS AND CONDITIONS
+
+   APPENDIX: How to apply the Apache License to your work.
+
+      To apply the Apache License to your work, attach the following
+      boilerplate notice, with the fields enclosed by brackets "[]"
+      replaced with your own identifying information. (Don't include
+      the brackets!)  The text should be enclosed in the appropriate
+      comment syntax for the file format. We also recommend that a
+      file or class name and description of purpose be included on the
+      same "printed page" as the copyright notice for easier
+      identification within third-party archives.
+
+   Copyright [yyyy] [name of copyright owner]
+
+   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.
diff --git a/third_party/scala/BUILD b/third_party/scala/BUILD
new file mode 100644
index 0000000..9786523
--- /dev/null
+++ b/third_party/scala/BUILD
@@ -0,0 +1,25 @@
+package(default_visibility = ["//visibility:public"])
+
+licenses(["notice"])
+
+load("//intellij_platform_sdk:build_defs.bzl", "select_for_plugin_api")
+
+java_library(
+    name = "scala_internal",
+    visibility = ["//visibility:private"],
+    exports = select_for_plugin_api({
+        "intellij-2017.1.1": ["@scala_2017_1//:scala"],
+    }),
+)
+
+java_library(
+    name = "scala_for_tests",
+    testonly = 1,
+    exports = [":scala_internal"],
+)
+
+java_library(
+    name = "scala",
+    neverlink = 1,
+    exports = [":scala_internal"],
+)
diff --git a/third_party/scala/LICENSE b/third_party/scala/LICENSE
new file mode 100644
index 0000000..d645695
--- /dev/null
+++ b/third_party/scala/LICENSE
@@ -0,0 +1,202 @@
+
+                                 Apache License
+                           Version 2.0, January 2004
+                        http://www.apache.org/licenses/
+
+   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+   1. Definitions.
+
+      "License" shall mean the terms and conditions for use, reproduction,
+      and distribution as defined by Sections 1 through 9 of this document.
+
+      "Licensor" shall mean the copyright owner or entity authorized by
+      the copyright owner that is granting the License.
+
+      "Legal Entity" shall mean the union of the acting entity and all
+      other entities that control, are controlled by, or are under common
+      control with that entity. For the purposes of this definition,
+      "control" means (i) the power, direct or indirect, to cause the
+      direction or management of such entity, whether by contract or
+      otherwise, or (ii) ownership of fifty percent (50%) or more of the
+      outstanding shares, or (iii) beneficial ownership of such entity.
+
+      "You" (or "Your") shall mean an individual or Legal Entity
+      exercising permissions granted by this License.
+
+      "Source" form shall mean the preferred form for making modifications,
+      including but not limited to software source code, documentation
+      source, and configuration files.
+
+      "Object" form shall mean any form resulting from mechanical
+      transformation or translation of a Source form, including but
+      not limited to compiled object code, generated documentation,
+      and conversions to other media types.
+
+      "Work" shall mean the work of authorship, whether in Source or
+      Object form, made available under the License, as indicated by a
+      copyright notice that is included in or attached to the work
+      (an example is provided in the Appendix below).
+
+      "Derivative Works" shall mean any work, whether in Source or Object
+      form, that is based on (or derived from) the Work and for which the
+      editorial revisions, annotations, elaborations, or other modifications
+      represent, as a whole, an original work of authorship. For the purposes
+      of this License, Derivative Works shall not include works that remain
+      separable from, or merely link (or bind by name) to the interfaces of,
+      the Work and Derivative Works thereof.
+
+      "Contribution" shall mean any work of authorship, including
+      the original version of the Work and any modifications or additions
+      to that Work or Derivative Works thereof, that is intentionally
+      submitted to Licensor for inclusion in the Work by the copyright owner
+      or by an individual or Legal Entity authorized to submit on behalf of
+      the copyright owner. For the purposes of this definition, "submitted"
+      means any form of electronic, verbal, or written communication sent
+      to the Licensor or its representatives, including but not limited to
+      communication on electronic mailing lists, source code control systems,
+      and issue tracking systems that are managed by, or on behalf of, the
+      Licensor for the purpose of discussing and improving the Work, but
+      excluding communication that is conspicuously marked or otherwise
+      designated in writing by the copyright owner as "Not a Contribution."
+
+      "Contributor" shall mean Licensor and any individual or Legal Entity
+      on behalf of whom a Contribution has been received by Licensor and
+      subsequently incorporated within the Work.
+
+   2. Grant of Copyright License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      copyright license to reproduce, prepare Derivative Works of,
+      publicly display, publicly perform, sublicense, and distribute the
+      Work and such Derivative Works in Source or Object form.
+
+   3. Grant of Patent License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      (except as stated in this section) patent license to make, have made,
+      use, offer to sell, sell, import, and otherwise transfer the Work,
+      where such license applies only to those patent claims licensable
+      by such Contributor that are necessarily infringed by their
+      Contribution(s) alone or by combination of their Contribution(s)
+      with the Work to which such Contribution(s) was submitted. If You
+      institute patent litigation against any entity (including a
+      cross-claim or counterclaim in a lawsuit) alleging that the Work
+      or a Contribution incorporated within the Work constitutes direct
+      or contributory patent infringement, then any patent licenses
+      granted to You under this License for that Work shall terminate
+      as of the date such litigation is filed.
+
+   4. Redistribution. You may reproduce and distribute copies of the
+      Work or Derivative Works thereof in any medium, with or without
+      modifications, and in Source or Object form, provided that You
+      meet the following conditions:
+
+      (a) You must give any other recipients of the Work or
+          Derivative Works a copy of this License; and
+
+      (b) You must cause any modified files to carry prominent notices
+          stating that You changed the files; and
+
+      (c) You must retain, in the Source form of any Derivative Works
+          that You distribute, all copyright, patent, trademark, and
+          attribution notices from the Source form of the Work,
+          excluding those notices that do not pertain to any part of
+          the Derivative Works; and
+
+      (d) If the Work includes a "NOTICE" text file as part of its
+          distribution, then any Derivative Works that You distribute must
+          include a readable copy of the attribution notices contained
+          within such NOTICE file, excluding those notices that do not
+          pertain to any part of the Derivative Works, in at least one
+          of the following places: within a NOTICE text file distributed
+          as part of the Derivative Works; within the Source form or
+          documentation, if provided along with the Derivative Works; or,
+          within a display generated by the Derivative Works, if and
+          wherever such third-party notices normally appear. The contents
+          of the NOTICE file are for informational purposes only and
+          do not modify the License. You may add Your own attribution
+          notices within Derivative Works that You distribute, alongside
+          or as an addendum to the NOTICE text from the Work, provided
+          that such additional attribution notices cannot be construed
+          as modifying the License.
+
+      You may add Your own copyright statement to Your modifications and
+      may provide additional or different license terms and conditions
+      for use, reproduction, or distribution of Your modifications, or
+      for any such Derivative Works as a whole, provided Your use,
+      reproduction, and distribution of the Work otherwise complies with
+      the conditions stated in this License.
+
+   5. Submission of Contributions. Unless You explicitly state otherwise,
+      any Contribution intentionally submitted for inclusion in the Work
+      by You to the Licensor shall be under the terms and conditions of
+      this License, without any additional terms or conditions.
+      Notwithstanding the above, nothing herein shall supersede or modify
+      the terms of any separate license agreement you may have executed
+      with Licensor regarding such Contributions.
+
+   6. Trademarks. This License does not grant permission to use the trade
+      names, trademarks, service marks, or product names of the Licensor,
+      except as required for reasonable and customary use in describing the
+      origin of the Work and reproducing the content of the NOTICE file.
+
+   7. Disclaimer of Warranty. Unless required by applicable law or
+      agreed to in writing, Licensor provides the Work (and each
+      Contributor provides its Contributions) on an "AS IS" BASIS,
+      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+      implied, including, without limitation, any warranties or conditions
+      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+      PARTICULAR PURPOSE. You are solely responsible for determining the
+      appropriateness of using or redistributing the Work and assume any
+      risks associated with Your exercise of permissions under this License.
+
+   8. Limitation of Liability. In no event and under no legal theory,
+      whether in tort (including negligence), contract, or otherwise,
+      unless required by applicable law (such as deliberate and grossly
+      negligent acts) or agreed to in writing, shall any Contributor be
+      liable to You for damages, including any direct, indirect, special,
+      incidental, or consequential damages of any character arising as a
+      result of this License or out of the use or inability to use the
+      Work (including but not limited to damages for loss of goodwill,
+      work stoppage, computer failure or malfunction, or any and all
+      other commercial damages or losses), even if such Contributor
+      has been advised of the possibility of such damages.
+
+   9. Accepting Warranty or Additional Liability. While redistributing
+      the Work or Derivative Works thereof, You may choose to offer,
+      and charge a fee for, acceptance of support, warranty, indemnity,
+      or other liability obligations and/or rights consistent with this
+      License. However, in accepting such obligations, You may act only
+      on Your own behalf and on Your sole responsibility, not on behalf
+      of any other Contributor, and only if You agree to indemnify,
+      defend, and hold each Contributor harmless for any liability
+      incurred by, or claims asserted against, such Contributor by reason
+      of your accepting any such warranty or additional liability.
+
+   END OF TERMS AND CONDITIONS
+
+   APPENDIX: How to apply the Apache License to your work.
+
+      To apply the Apache License to your work, attach the following
+      boilerplate notice, with the fields enclosed by brackets "[]"
+      replaced with your own identifying information. (Don't include
+      the brackets!)  The text should be enclosed in the appropriate
+      comment syntax for the file format. We also recommend that a
+      file or class name and description of purpose be included on the
+      same "printed page" as the copyright notice for easier
+      identification within third-party archives.
+
+   Copyright [yyyy] [name of copyright owner]
+
+   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.
diff --git a/third_party/zip-wrap/unzip.sh b/third_party/zip-wrap/unzip.sh
new file mode 100755
index 0000000..f1be34d
--- /dev/null
+++ b/third_party/zip-wrap/unzip.sh
@@ -0,0 +1,15 @@
+# Copyright 2015 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.
+
+unzip "$@"
diff --git a/version.bzl b/version.bzl
index 85514a2..667e07a 100644
--- a/version.bzl
+++ b/version.bzl
@@ -1,3 +1,3 @@
 """Version of the blaze plugin."""
 
-VERSION = "2017.01.30.4"
+VERSION = "2017.05.17.1"