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 {