Project import generated by Copybara.
diff --git a/BUILD b/BUILD
index 9d98461..81608fe 100644
--- a/BUILD
+++ b/BUILD
@@ -31,6 +31,7 @@
     name = "clwb_tests",
     tests = [
         "//base:unit_tests",
+        "//cpp:unit_tests",
     ],
 )
 
diff --git a/WORKSPACE b/WORKSPACE
index f443141..485d46f 100644
--- a/WORKSPACE
+++ b/WORKSPACE
@@ -5,7 +5,7 @@
 new_http_archive(
     name = "intellij_latest",
     build_file = "intellij_platform_sdk/BUILD.idea",
-    url = "https://download.jetbrains.com/idea/ideaIC-2016.2.1.tar.gz",
+    url = "https://download.jetbrains.com/idea/ideaIC-2016.2.4.tar.gz",
 )
 
 # The plugin api for CLion 2016.1.3. This is required to build CLwB,
@@ -13,7 +13,7 @@
 new_http_archive(
     name = "clion_latest",
     build_file = "intellij_platform_sdk/BUILD.clion",
-    url = "https://download.jetbrains.com/cpp/CLion-2016.2.1.tar.gz",
+    url = "https://download.jetbrains.com/cpp/CLion-2016.2.2.tar.gz",
 )
 
 # The plugin api for Android Studio 2.2 stable. This is required to build ASwB,
diff --git a/aswb/BUILD b/aswb/BUILD
index 2312a96..6e4a3a2 100644
--- a/aswb/BUILD
+++ b/aswb/BUILD
@@ -61,7 +61,7 @@
 )
 
 load(
-    "//intellij_test:test_defs.bzl",
+    "//testing:test_defs.bzl",
     "intellij_unit_test_suite",
 )
 
diff --git a/aswb/.bazelproject b/aswb/aswb.bazelproject
similarity index 70%
rename from aswb/.bazelproject
rename to aswb/aswb.bazelproject
index 2fdb3b8..34cbb52 100644
--- a/aswb/.bazelproject
+++ b/aswb/aswb.bazelproject
@@ -1,13 +1,12 @@
 directories:
   .
   -ijwb
-  -blaze-plugin-dev
+  -plugin_dev
   -clwb
-  -blaze-cpp/src/com/google/idea/blaze/cpp/versioned/v162
+  -cpp/src/com/google/idea/blaze/cpp/versioned/v162
 
 targets:
   //aswb:aswb_bazel
-  //aswb:aswb_blaze
   //:aswb_tests
 
 workspace_type: intellij_plugin
diff --git a/aswb/src/META-INF/aswb.xml b/aswb/src/META-INF/aswb.xml
index f7b42c3..d8d9334 100644
--- a/aswb/src/META-INF/aswb.xml
+++ b/aswb/src/META-INF/aswb.xml
@@ -51,7 +51,7 @@
     <SyncListener implementation="com.google.idea.blaze.android.sync.BlazeAndroidSyncListener"/>
     <SyncListener implementation="com.google.idea.blaze.android.cppimpl.BlazeNdkSupportEnabler"/>
     <SyncListener implementation="com.google.idea.blaze.android.manifest.ManifestParser$ClearManifestParser"/>
-    <RuleConfigurationFactory implementation="com.google.idea.blaze.android.run.BlazeAndroidRuleConfigurationFactory"/>
+    <RunConfigurationFactory implementation="com.google.idea.blaze.android.run.BlazeAndroidRunConfigurationFactory"/>
     <java.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"/>
diff --git a/aswb/src/META-INF/aswb_bazel.xml b/aswb/src/META-INF/aswb_bazel.xml
index 4b89d60..4d50f16 100644
--- a/aswb/src/META-INF/aswb_bazel.xml
+++ b/aswb/src/META-INF/aswb_bazel.xml
@@ -14,5 +14,18 @@
   ~ limitations under the License.
   -->
 <idea-plugin>
-  <description>Provides the ability to import Bazel projects in Android Studio.</description>
+  <description>
+    <![CDATA[
+      <a href="http://bazel.io">Bazel</a> support for Android Studio.
+
+      Features:
+        <ul>
+        <li>Import BUILD files into the IDE.</li>
+        <li>BUILD file custom language support.</li>
+        <li>Support for Bazel run configurations for certain rule classes.</li>
+        </ul>
+
+      Usage instructions at <a href="http://ij.bazel.io">ij.bazel.io</a>
+      ]]>
+  </description>
 </idea-plugin>
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 1918b2c..3087e9a 100644
--- a/aswb/src/com/google/idea/blaze/android/cppimpl/BlazeNdkSupportEnabler.java
+++ b/aswb/src/com/google/idea/blaze/android/cppimpl/BlazeNdkSupportEnabler.java
@@ -21,6 +21,7 @@
 import com.google.idea.blaze.base.model.BlazeProjectData;
 import com.google.idea.blaze.base.model.primitives.LanguageClass;
 import com.google.idea.blaze.base.projectview.ProjectViewSet;
+import com.google.idea.blaze.base.scope.BlazeContext;
 import com.google.idea.blaze.base.settings.BlazeImportSettings;
 import com.google.idea.blaze.base.sync.SyncListener;
 import com.google.idea.blaze.cpp.BlazeCWorkspace;
@@ -34,6 +35,7 @@
   @Override
   public void onSyncComplete(
       Project project,
+      BlazeContext context,
       BlazeImportSettings importSettings,
       ProjectViewSet projectViewSet,
       BlazeProjectData blazeProjectData,
diff --git a/aswb/src/com/google/idea/blaze/android/manifest/ManifestParser.java b/aswb/src/com/google/idea/blaze/android/manifest/ManifestParser.java
index 00cbe6b..a8fe734 100644
--- a/aswb/src/com/google/idea/blaze/android/manifest/ManifestParser.java
+++ b/aswb/src/com/google/idea/blaze/android/manifest/ManifestParser.java
@@ -18,6 +18,7 @@
 import com.google.common.collect.Maps;
 import com.google.idea.blaze.base.model.BlazeProjectData;
 import com.google.idea.blaze.base.projectview.ProjectViewSet;
+import com.google.idea.blaze.base.scope.BlazeContext;
 import com.google.idea.blaze.base.settings.BlazeImportSettings;
 import com.google.idea.blaze.base.sync.SyncListener;
 import com.intellij.openapi.application.ApplicationManager;
@@ -98,6 +99,7 @@
     @Override
     public void onSyncComplete(
         Project project,
+        BlazeContext context,
         BlazeImportSettings importSettings,
         ProjectViewSet projectViewSet,
         BlazeProjectData blazeProjectData,
diff --git a/aswb/src/com/google/idea/blaze/android/resources/AndroidPackageRClass.java b/aswb/src/com/google/idea/blaze/android/resources/AndroidPackageRClass.java
index b1802aa..66f42df 100644
--- a/aswb/src/com/google/idea/blaze/android/resources/AndroidPackageRClass.java
+++ b/aswb/src/com/google/idea/blaze/android/resources/AndroidPackageRClass.java
@@ -16,7 +16,6 @@
 package com.google.idea.blaze.android.resources;
 
 import com.android.resources.ResourceType;
-import com.google.idea.common.experiments.BoolExperiment;
 import com.intellij.ide.highlighter.JavaFileType;
 import com.intellij.openapi.diagnostic.Logger;
 import com.intellij.openapi.module.Module;
@@ -27,7 +26,7 @@
 import com.intellij.psi.PsiFileFactory;
 import com.intellij.psi.PsiManager;
 import com.intellij.psi.util.CachedValue;
-import com.intellij.psi.util.CachedValueProvider;
+import com.intellij.psi.util.CachedValueProvider.Result;
 import com.intellij.psi.util.CachedValuesManager;
 import com.intellij.psi.util.PsiModificationTracker;
 import java.util.ArrayList;
@@ -44,8 +43,6 @@
 /** Represents a dynamic "class R" for resources in an Android module. */
 public class AndroidPackageRClass extends AndroidLightClassBase {
   private static final Logger LOG = Logger.getInstance(AndroidPackageRClass.class);
-  private static final BoolExperiment USE_OUT_OF_CODE_MOD_COUNT =
-      new BoolExperiment("use.out.of.code.modcount.for.r.class.cache", true);
 
   @NotNull private final PsiFile myFile;
   @NotNull private final String myFullyQualifiedName;
@@ -108,16 +105,10 @@
       myClassCache =
           CachedValuesManager.getManager(getProject())
               .createCachedValue(
-                  new CachedValueProvider<PsiClass[]>() {
-                    @Override
-                    public Result<PsiClass[]> compute() {
-                      return Result.create(
+                  () ->
+                      Result.create(
                           doGetInnerClasses(),
-                          USE_OUT_OF_CODE_MOD_COUNT.getValue()
-                              ? PsiModificationTracker.OUT_OF_CODE_BLOCK_MODIFICATION_COUNT
-                              : PsiModificationTracker.MODIFICATION_COUNT);
-                    }
-                  });
+                          PsiModificationTracker.OUT_OF_CODE_BLOCK_MODIFICATION_COUNT));
     }
     return myClassCache.getValue();
   }
@@ -136,7 +127,7 @@
 
     final Set<ResourceType> types =
         ResourceReferenceConverter.getResourceTypesInCurrentModule(facet);
-    final List<PsiClass> result = new ArrayList<PsiClass>();
+    final List<PsiClass> result = new ArrayList<>();
 
     for (ResourceType type : types) {
       result.add(new ResourceTypeClass(facet, type.getName(), this));
diff --git a/aswb/src/com/google/idea/blaze/android/resources/actions/BlazeCreateResourceUtils.java b/aswb/src/com/google/idea/blaze/android/resources/actions/BlazeCreateResourceUtils.java
index 2b98ee9..20f88d2 100644
--- a/aswb/src/com/google/idea/blaze/android/resources/actions/BlazeCreateResourceUtils.java
+++ b/aswb/src/com/google/idea/blaze/android/resources/actions/BlazeCreateResourceUtils.java
@@ -21,10 +21,11 @@
 import com.google.common.collect.Sets;
 import com.google.idea.blaze.android.sync.model.AndroidResourceModule;
 import com.google.idea.blaze.android.sync.model.BlazeAndroidSyncData;
+import com.google.idea.blaze.base.ideinfo.RuleKey;
 import com.google.idea.blaze.base.model.BlazeProjectData;
-import com.google.idea.blaze.base.model.primitives.Label;
 import com.google.idea.blaze.base.rulemaps.SourceToRuleMap;
 import com.google.idea.blaze.base.sync.data.BlazeProjectDataManager;
+import com.google.idea.blaze.base.sync.workspace.ArtifactLocationDecoder;
 import com.intellij.ide.util.DirectoryUtil;
 import com.intellij.openapi.application.ApplicationManager;
 import com.intellij.openapi.project.Project;
@@ -37,6 +38,7 @@
 import com.intellij.ui.ComboboxWithBrowseButton;
 import com.intellij.ui.components.JBLabel;
 import java.io.File;
+import java.util.Collection;
 import java.util.Set;
 import javax.swing.JComboBox;
 import org.jetbrains.annotations.Nullable;
@@ -59,16 +61,19 @@
     if (blazeProjectData != null) {
       BlazeAndroidSyncData syncData = blazeProjectData.syncState.get(BlazeAndroidSyncData.class);
       if (syncData != null) {
-        ImmutableCollection<Label> labelsRelatedToContext = null;
+        ImmutableCollection<RuleKey> rulesRelatedToContext = null;
         File fileFromContext = null;
         if (contextFile != null) {
           fileFromContext = VfsUtilCore.virtualToIoFile(contextFile);
-          labelsRelatedToContext =
-              SourceToRuleMap.getInstance(project).getTargetsForSourceFile(fileFromContext);
-          if (labelsRelatedToContext.isEmpty()) {
-            labelsRelatedToContext = null;
+          rulesRelatedToContext =
+              SourceToRuleMap.getInstance(project).getRulesForSourceFile(fileFromContext);
+          if (rulesRelatedToContext.isEmpty()) {
+            rulesRelatedToContext = null;
           }
         }
+
+        ArtifactLocationDecoder artifactLocationDecoder = blazeProjectData.artifactLocationDecoder;
+
         // Sort:
         // - contextFile/res if contextFile is a directory,
         //   to optimize the right click on directory case, or the "closest" string
@@ -81,20 +86,24 @@
         Set<File> allResDirs = Sets.newTreeSet();
         for (AndroidResourceModule androidResourceModule :
             syncData.importResult.androidResourceModules) {
+
+          Collection<File> resources =
+              artifactLocationDecoder.decodeAll(androidResourceModule.resources);
+
+          Collection<File> transitiveResources =
+              artifactLocationDecoder.decodeAll(androidResourceModule.transitiveResources);
+
           // labelsRelatedToContext should include deps,
           // but as a first pass we only check the rules themselves
           // for resources. If we come up empty, then have anyResDir as a backup.
-          allResDirs.addAll(androidResourceModule.transitiveResources);
-          if (labelsRelatedToContext != null
-              && !labelsRelatedToContext.contains(androidResourceModule.label)) {
+          allResDirs.addAll(transitiveResources);
+
+          if (rulesRelatedToContext != null
+              && !rulesRelatedToContext.contains(androidResourceModule.ruleKey)) {
             continue;
           }
-          for (File resDir : androidResourceModule.resources) {
-            resourceDirs.add(resDir);
-          }
-          for (File resDir : androidResourceModule.transitiveResources) {
-            transitiveDirs.add(resDir);
-          }
+          resourceDirs.addAll(resources);
+          transitiveDirs.addAll(transitiveResources);
         }
         // No need to show some directories twice.
         transitiveDirs.removeAll(resourceDirs);
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 2acea9b..ed7a367 100644
--- a/aswb/src/com/google/idea/blaze/android/run/BlazeAndroidRunConfigurationCommonState.java
+++ b/aswb/src/com/google/idea/blaze/android/run/BlazeAndroidRunConfigurationCommonState.java
@@ -17,34 +17,68 @@
 
 import static com.google.idea.blaze.android.cppapi.NdkSupport.NDK_SUPPORT;
 
+import com.android.tools.idea.run.ValidationError;
 import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Lists;
+import com.google.idea.blaze.android.cppapi.NdkSupport;
+import com.google.idea.blaze.android.run.runner.BlazeAndroidRunConfigurationDebuggerManager;
+import com.google.idea.blaze.android.run.runner.BlazeAndroidRunConfigurationDeployTargetManager;
+import com.google.idea.blaze.base.command.BlazeFlags;
+import com.google.idea.blaze.base.projectview.ProjectViewSet;
+import com.google.idea.blaze.base.run.state.RunConfigurationFlagsState;
+import com.google.idea.blaze.base.run.state.RunConfigurationState;
+import com.google.idea.blaze.base.run.state.RunConfigurationStateEditor;
+import com.google.idea.blaze.base.ui.UiUtil;
+import com.intellij.openapi.project.Project;
 import com.intellij.openapi.util.InvalidDataException;
 import com.intellij.openapi.util.WriteExternalException;
+import java.awt.Component;
 import java.util.List;
+import javax.annotation.Nullable;
+import javax.swing.JCheckBox;
+import javax.swing.JComponent;
 import org.jdom.Element;
+import org.jetbrains.android.facet.AndroidFacet;
 
-/**
- * A shared state class for run configurations targeting Blaze Android rules. We implement the
- * deprecated JDomExternalizable to fit with the other run configs.
- */
-public class BlazeAndroidRunConfigurationCommonState implements BlazeAndroidRunConfigurationState {
+/** A shared state class for run configurations targeting Blaze Android rules. */
+public class BlazeAndroidRunConfigurationCommonState implements RunConfigurationState {
   private static final String USER_FLAG_TAG = "blaze-user-flag";
   private static final String NATIVE_DEBUG_ATTR = "blaze-native-debug";
 
-  private List<String> userFlags;
+  // We need to split "-c dbg" into two flags because we pass flags
+  // as a list of strings to the command line executor and we need blaze
+  // to see -c and dbg as two separate entities, not one.
+  private static final ImmutableList<String> NATIVE_DEBUG_FLAGS =
+      ImmutableList.of("--fission=no", "-c", "dbg");
+
+  private final BlazeAndroidRunConfigurationDeployTargetManager deployTargetManager;
+  private final BlazeAndroidRunConfigurationDebuggerManager debuggerManager;
+
+  private final RunConfigurationFlagsState userFlags;
   private boolean nativeDebuggingEnabled = false;
 
-  /** Creates a configuration state initialized with the given flags. */
-  public BlazeAndroidRunConfigurationCommonState(List<String> userFlags) {
-    this.userFlags = userFlags;
+  public BlazeAndroidRunConfigurationCommonState(String buildSystemName, boolean isAndroidTest) {
+    this.deployTargetManager = new BlazeAndroidRunConfigurationDeployTargetManager(isAndroidTest);
+    this.debuggerManager = new BlazeAndroidRunConfigurationDebuggerManager(this);
+    this.userFlags =
+        new RunConfigurationFlagsState(
+            USER_FLAG_TAG, String.format("Custom %s build flags:", buildSystemName));
+  }
+
+  public BlazeAndroidRunConfigurationDeployTargetManager getDeployTargetManager() {
+    return deployTargetManager;
+  }
+
+  public BlazeAndroidRunConfigurationDebuggerManager getDebuggerManager() {
+    return debuggerManager;
   }
 
   public List<String> getUserFlags() {
-    return userFlags;
+    return userFlags.getFlags();
   }
 
   public void setUserFlags(List<String> userFlags) {
-    this.userFlags = userFlags;
+    this.userFlags.setFlags(userFlags);
   }
 
   public boolean isNativeDebuggingEnabled() {
@@ -55,26 +89,91 @@
     this.nativeDebuggingEnabled = nativeDebuggingEnabled;
   }
 
+  public ImmutableList<String> getBuildFlags(Project project, ProjectViewSet projectViewSet) {
+    return ImmutableList.<String>builder()
+        .addAll(BlazeFlags.buildFlags(project, projectViewSet))
+        .addAll(getUserFlags())
+        .addAll(getNativeDebuggerFlags())
+        .build();
+  }
+
+  private ImmutableList<String> getNativeDebuggerFlags() {
+    return isNativeDebuggingEnabled() ? NATIVE_DEBUG_FLAGS : ImmutableList.of();
+  }
+
+  /**
+   * We collect errors rather than throwing to avoid missing fatal errors by exiting early for a
+   * warning.
+   */
+  public List<ValidationError> validate(@Nullable AndroidFacet facet) {
+    List<ValidationError> errors = Lists.newArrayList();
+    // If facet is null, we can't validate the managers, but that's fine because
+    // BlazeAndroidRunConfigurationValidationUtil.validateFacet will give a fatal error.
+    if (facet != null) {
+      errors.addAll(deployTargetManager.validate(facet));
+      errors.addAll(debuggerManager.validate(facet));
+    }
+    return errors;
+  }
+
   @Override
   public void readExternal(Element element) throws InvalidDataException {
-    ImmutableList.Builder<String> flagsBuilder = ImmutableList.builder();
-    for (Element e : element.getChildren(USER_FLAG_TAG)) {
-      String flag = e.getTextTrim();
-      if (flag != null && !flag.isEmpty()) {
-        flagsBuilder.add(flag);
-      }
-    }
-    userFlags = flagsBuilder.build();
+    userFlags.readExternal(element);
     setNativeDebuggingEnabled(Boolean.parseBoolean(element.getAttributeValue(NATIVE_DEBUG_ATTR)));
+
+    deployTargetManager.readExternal(element);
+    debuggerManager.readExternal(element);
   }
 
   @Override
   public void writeExternal(Element element) throws WriteExternalException {
-    for (String flag : userFlags) {
-      Element child = new Element(USER_FLAG_TAG);
-      child.setText(flag);
-      element.addContent(child);
-    }
+    userFlags.writeExternal(element);
     element.setAttribute(NATIVE_DEBUG_ATTR, Boolean.toString(nativeDebuggingEnabled));
+
+    deployTargetManager.writeExternal(element);
+    debuggerManager.writeExternal(element);
+  }
+
+  @Override
+  public RunConfigurationStateEditor getEditor(Project project) {
+    return new BlazeAndroidRunConfigurationCommonStateEditor(this, project);
+  }
+
+  private static class BlazeAndroidRunConfigurationCommonStateEditor
+      implements RunConfigurationStateEditor {
+
+    private final RunConfigurationStateEditor userFlagsEditor;
+    private final JCheckBox enableNativeDebuggingCheckBox;
+
+    BlazeAndroidRunConfigurationCommonStateEditor(
+        BlazeAndroidRunConfigurationCommonState state, Project project) {
+      userFlagsEditor = state.userFlags.getEditor(project);
+      enableNativeDebuggingCheckBox = new JCheckBox("Enable native debugging", false);
+    }
+
+    @Override
+    public void resetEditorFrom(RunConfigurationState genericState) {
+      BlazeAndroidRunConfigurationCommonState state =
+          (BlazeAndroidRunConfigurationCommonState) genericState;
+      userFlagsEditor.resetEditorFrom(state.userFlags);
+      enableNativeDebuggingCheckBox.setSelected(state.isNativeDebuggingEnabled());
+    }
+
+    @Override
+    public void applyEditorTo(RunConfigurationState genericState) {
+      BlazeAndroidRunConfigurationCommonState state =
+          (BlazeAndroidRunConfigurationCommonState) genericState;
+      userFlagsEditor.applyEditorTo(state.userFlags);
+      state.setNativeDebuggingEnabled(enableNativeDebuggingCheckBox.isSelected());
+    }
+
+    @Override
+    public JComponent createComponent() {
+      List<Component> result = Lists.newArrayList(userFlagsEditor.createComponent());
+      if (NdkSupport.NDK_SUPPORT.getValue()) {
+        result.add(enableNativeDebuggingCheckBox);
+      }
+      return UiUtil.createBox(result);
+    }
   }
 }
diff --git a/aswb/src/com/google/idea/blaze/android/run/BlazeAndroidRunConfigurationCommonStateEditor.java b/aswb/src/com/google/idea/blaze/android/run/BlazeAndroidRunConfigurationCommonStateEditor.java
deleted file mode 100644
index 3a0c511..0000000
--- a/aswb/src/com/google/idea/blaze/android/run/BlazeAndroidRunConfigurationCommonStateEditor.java
+++ /dev/null
@@ -1,69 +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 com.google.common.base.Strings;
-import com.google.common.collect.Lists;
-import com.google.idea.blaze.android.cppapi.NdkSupport;
-import com.google.idea.blaze.base.settings.Blaze;
-import com.intellij.openapi.project.Project;
-import com.intellij.util.execution.ParametersListUtil;
-import java.util.List;
-import javax.swing.JCheckBox;
-import javax.swing.JComponent;
-import javax.swing.JLabel;
-import javax.swing.JTextArea;
-
-/**
- * A simplified, Blaze-specific variant of {@link
- * org.jetbrains.android.run.AndroidRunConfigurationEditor}.
- */
-public class BlazeAndroidRunConfigurationCommonStateEditor {
-  private final Project project;
-  private final JTextArea userFlagsField;
-  private final JCheckBox enableNativeDebuggingCheckBox;
-
-  public BlazeAndroidRunConfigurationCommonStateEditor(Project project) {
-    this.project = project;
-
-    userFlagsField = new JTextArea(3 /* rows */, 50 /* columns */);
-    userFlagsField.setToolTipText("e.g. --config=android_arm");
-    enableNativeDebuggingCheckBox = new JCheckBox("Enable native debugging", false);
-  }
-
-  public void resetEditorFrom(BlazeAndroidRunConfigurationCommonState runConfigurationState) {
-    userFlagsField.setText(ParametersListUtil.join(runConfigurationState.getUserFlags()));
-    enableNativeDebuggingCheckBox.setSelected(runConfigurationState.isNativeDebuggingEnabled());
-  }
-
-  public void applyEditorTo(BlazeAndroidRunConfigurationCommonState runConfigurationState) {
-    List<String> userFlags =
-        ParametersListUtil.parse(Strings.nullToEmpty(userFlagsField.getText()));
-    runConfigurationState.setUserFlags(userFlags);
-    runConfigurationState.setNativeDebuggingEnabled(enableNativeDebuggingCheckBox.isSelected());
-  }
-
-  public List<JComponent> getComponents() {
-    List<JComponent> result =
-        Lists.newArrayList(
-            new JLabel(String.format("Custom %s build flags:", Blaze.buildSystemName(project))),
-            userFlagsField);
-    if (NdkSupport.NDK_SUPPORT.getValue()) {
-      result.add(enableNativeDebuggingCheckBox);
-    }
-    return result;
-  }
-}
diff --git a/aswb/src/com/google/idea/blaze/android/run/BlazeAndroidRuleConfigurationFactory.java b/aswb/src/com/google/idea/blaze/android/run/BlazeAndroidRunConfigurationFactory.java
similarity index 68%
rename from aswb/src/com/google/idea/blaze/android/run/BlazeAndroidRuleConfigurationFactory.java
rename to aswb/src/com/google/idea/blaze/android/run/BlazeAndroidRunConfigurationFactory.java
index 008982a..d398ece 100644
--- a/aswb/src/com/google/idea/blaze/android/run/BlazeAndroidRuleConfigurationFactory.java
+++ b/aswb/src/com/google/idea/blaze/android/run/BlazeAndroidRunConfigurationFactory.java
@@ -16,20 +16,23 @@
 package com.google.idea.blaze.android.run;
 
 import com.google.idea.blaze.base.ideinfo.RuleIdeInfo;
+import com.google.idea.blaze.base.ideinfo.RuleKey;
+import com.google.idea.blaze.base.model.BlazeProjectData;
 import com.google.idea.blaze.base.model.primitives.Kind;
+import com.google.idea.blaze.base.model.primitives.Label;
 import com.google.idea.blaze.base.run.BlazeCommandRunConfiguration;
 import com.google.idea.blaze.base.run.BlazeCommandRunConfigurationType;
-import com.google.idea.blaze.base.run.BlazeRuleConfigurationFactory;
-import com.google.idea.blaze.base.sync.projectview.WorkspaceLanguageSettings;
+import com.google.idea.blaze.base.run.BlazeRunConfigurationFactory;
 import com.intellij.execution.configurations.ConfigurationFactory;
 import com.intellij.execution.configurations.RunConfiguration;
+import com.intellij.openapi.project.Project;
 
 /** Creates run configurations for android_binary and android_test. */
-public class BlazeAndroidRuleConfigurationFactory extends BlazeRuleConfigurationFactory {
+public class BlazeAndroidRunConfigurationFactory extends BlazeRunConfigurationFactory {
   @Override
-  public boolean handlesRule(
-      WorkspaceLanguageSettings workspaceLanguageSettings, RuleIdeInfo rule) {
-    return rule.kindIsOneOf(Kind.ANDROID_BINARY, Kind.ANDROID_TEST);
+  public boolean handlesTarget(Project project, BlazeProjectData blazeProjectData, Label target) {
+    RuleIdeInfo rule = blazeProjectData.ruleMap.get(RuleKey.forPlainTarget(target));
+    return rule != null && rule.kindIsOneOf(Kind.ANDROID_BINARY, Kind.ANDROID_TEST);
   }
 
   @Override
@@ -38,9 +41,9 @@
   }
 
   @Override
-  public void setupConfiguration(RunConfiguration configuration, RuleIdeInfo rule) {
+  public void setupConfiguration(RunConfiguration configuration, Label target) {
     final BlazeCommandRunConfiguration blazeConfig = (BlazeCommandRunConfiguration) configuration;
-    blazeConfig.setTarget(rule.label);
+    blazeConfig.setTarget(target);
     blazeConfig.setGeneratedName();
   }
 }
diff --git a/aswb/src/com/google/idea/blaze/android/run/BlazeAndroidRunConfigurationHandler.java b/aswb/src/com/google/idea/blaze/android/run/BlazeAndroidRunConfigurationHandler.java
index ed5a394..14f00b9 100644
--- a/aswb/src/com/google/idea/blaze/android/run/BlazeAndroidRunConfigurationHandler.java
+++ b/aswb/src/com/google/idea/blaze/android/run/BlazeAndroidRunConfigurationHandler.java
@@ -15,15 +15,10 @@
  */
 package com.google.idea.blaze.android.run;
 
-import com.google.common.collect.ImmutableList;
-import com.google.idea.blaze.android.run.runner.BlazeAndroidRunContext;
 import com.google.idea.blaze.base.model.primitives.Label;
 import com.google.idea.blaze.base.run.BlazeCommandRunConfiguration;
 import com.google.idea.blaze.base.run.confighandler.BlazeCommandRunConfigurationHandler;
 import com.intellij.execution.configurations.RunProfile;
-import com.intellij.execution.runners.ExecutionEnvironment;
-import com.intellij.openapi.project.Project;
-import org.jetbrains.android.facet.AndroidFacet;
 import org.jetbrains.annotations.Nullable;
 
 /** Common interface for Blaze Android run configuration handlers. */
@@ -39,16 +34,16 @@
     if (!(profile instanceof BlazeCommandRunConfiguration)) {
       return null;
     }
-    BlazeCommandRunConfiguration blazeConfiguration = (BlazeCommandRunConfiguration) profile;
-    return blazeConfiguration.getHandlerIfType(BlazeAndroidRunConfigurationHandler.class);
+    BlazeCommandRunConfigurationHandler handler =
+        ((BlazeCommandRunConfiguration) profile).getHandler();
+    if (!(handler instanceof BlazeAndroidRunConfigurationHandler)) {
+      return null;
+    }
+    return (BlazeAndroidRunConfigurationHandler) handler;
   }
 
-  /** @return A {@link BlazeAndroidRunContext} for this handler with the given settings. */
-  BlazeAndroidRunContext createRunContext(
-      Project project,
-      AndroidFacet facet,
-      ExecutionEnvironment env,
-      ImmutableList<String> buildFlags);
+  /** @return This handler's common state. */
+  BlazeAndroidRunConfigurationCommonState getCommonState();
 
   /**
    * @return The {@link Label} this handler's configuration targets, or null if it does not target a
@@ -56,10 +51,4 @@
    */
   @Nullable
   Label getLabel();
-
-  /** @return This handler's common state. */
-  BlazeAndroidRunConfigurationCommonState getCommonState();
-
-  /** @return This handler's type-specific state. */
-  BlazeAndroidRunConfigurationState getConfigState();
 }
diff --git a/aswb/src/com/google/idea/blaze/android/run/BlazeAndroidRunConfigurationHandlerEditor.java b/aswb/src/com/google/idea/blaze/android/run/BlazeAndroidRunConfigurationHandlerEditor.java
deleted file mode 100644
index e23f0a8..0000000
--- a/aswb/src/com/google/idea/blaze/android/run/BlazeAndroidRunConfigurationHandlerEditor.java
+++ /dev/null
@@ -1,65 +0,0 @@
-/*
- * Copyright 2016 The Bazel Authors. All rights reserved.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.google.idea.blaze.android.run;
-
-import com.google.common.collect.Lists;
-import com.google.idea.blaze.base.run.confighandler.BlazeCommandRunConfigurationHandler;
-import com.google.idea.blaze.base.run.confighandler.BlazeCommandRunConfigurationHandlerEditor;
-import com.google.idea.blaze.base.ui.UiUtil;
-import com.intellij.openapi.project.Project;
-import java.awt.Component;
-import java.util.List;
-import javax.swing.JComponent;
-import org.jetbrains.annotations.NotNull;
-
-/**
- * A simplified, Blaze-specific variant of {@link
- * org.jetbrains.android.run.AndroidRunConfigurationEditor}.
- */
-public class BlazeAndroidRunConfigurationHandlerEditor
-    implements BlazeCommandRunConfigurationHandlerEditor {
-  private final BlazeAndroidRunConfigurationCommonStateEditor commonStateEditor;
-  private final BlazeAndroidRunConfigurationStateEditor kindSpecificEditor;
-
-  public BlazeAndroidRunConfigurationHandlerEditor(
-      Project project, BlazeAndroidRunConfigurationStateEditor kindSpecificEditor) {
-    this.commonStateEditor = new BlazeAndroidRunConfigurationCommonStateEditor(project);
-    this.kindSpecificEditor = kindSpecificEditor;
-  }
-
-  @Override
-  public void resetEditorFrom(BlazeCommandRunConfigurationHandler h) {
-    BlazeAndroidRunConfigurationHandler handler = (BlazeAndroidRunConfigurationHandler) h;
-    commonStateEditor.resetEditorFrom(handler.getCommonState());
-    kindSpecificEditor.resetEditorFrom(handler.getConfigState());
-  }
-
-  @Override
-  public void applyEditorTo(BlazeCommandRunConfigurationHandler h) {
-    BlazeAndroidRunConfigurationHandler handler = (BlazeAndroidRunConfigurationHandler) h;
-    commonStateEditor.applyEditorTo(handler.getCommonState());
-    kindSpecificEditor.applyEditorTo(handler.getConfigState());
-  }
-
-  @Override
-  @NotNull
-  public JComponent createEditor() {
-    List<Component> components = Lists.newArrayList();
-    components.addAll(commonStateEditor.getComponents());
-    components.add(kindSpecificEditor.getComponent());
-    return UiUtil.createBox(components);
-  }
-}
diff --git a/aswb/src/com/google/idea/blaze/android/run/BlazeAndroidRunConfigurationState.java b/aswb/src/com/google/idea/blaze/android/run/BlazeAndroidRunConfigurationState.java
deleted file mode 100644
index 1d13b56..0000000
--- a/aswb/src/com/google/idea/blaze/android/run/BlazeAndroidRunConfigurationState.java
+++ /dev/null
@@ -1,21 +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 com.intellij.openapi.util.JDOMExternalizable;
-
-/** Indicates a class stores state for a Blaze Android run configuration. */
-public interface BlazeAndroidRunConfigurationState extends JDOMExternalizable {}
diff --git a/aswb/src/com/google/idea/blaze/android/run/BlazeAndroidRunConfigurationStateEditor.java b/aswb/src/com/google/idea/blaze/android/run/BlazeAndroidRunConfigurationStateEditor.java
deleted file mode 100644
index 7cbd815..0000000
--- a/aswb/src/com/google/idea/blaze/android/run/BlazeAndroidRunConfigurationStateEditor.java
+++ /dev/null
@@ -1,28 +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 java.awt.Component;
-
-/** An editor for Blaze Android run configuration state. */
-public interface BlazeAndroidRunConfigurationStateEditor {
-
-  void resetEditorFrom(BlazeAndroidRunConfigurationState state);
-
-  void applyEditorTo(BlazeAndroidRunConfigurationState state);
-
-  Component getComponent();
-}
diff --git a/aswb/src/com/google/idea/blaze/android/run/BlazeAndroidRunConfigurationValidationUtil.java b/aswb/src/com/google/idea/blaze/android/run/BlazeAndroidRunConfigurationValidationUtil.java
new file mode 100644
index 0000000..2c0c26e
--- /dev/null
+++ b/aswb/src/com/google/idea/blaze/android/run/BlazeAndroidRunConfigurationValidationUtil.java
@@ -0,0 +1,137 @@
+/*
+ * 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 com.android.tools.idea.gradle.util.Projects;
+import com.android.tools.idea.run.ValidationError;
+import com.google.common.collect.Lists;
+import com.google.common.collect.Ordering;
+import com.google.idea.blaze.base.ideinfo.RuleIdeInfo;
+import com.google.idea.blaze.base.model.primitives.Kind;
+import com.google.idea.blaze.base.model.primitives.Label;
+import com.google.idea.blaze.base.projectview.ProjectViewSet;
+import com.google.idea.blaze.base.run.rulefinder.RuleFinder;
+import com.google.idea.blaze.base.settings.Blaze;
+import com.intellij.execution.ExecutionException;
+import com.intellij.execution.configurations.RuntimeConfigurationError;
+import com.intellij.execution.configurations.RuntimeConfigurationException;
+import com.intellij.execution.configurations.RuntimeConfigurationWarning;
+import com.intellij.openapi.module.Module;
+import com.intellij.openapi.project.Project;
+import java.util.List;
+import javax.annotation.Nullable;
+import org.jetbrains.android.facet.AndroidFacet;
+import org.jetbrains.android.util.AndroidBundle;
+
+/**
+ * Utility class for validating {@link BlazeAndroidRunConfigurationHandler}s. We collect
+ * configuration errors rather than throwing to avoid missing fatal errors by exiting early for a
+ * warning.
+ */
+public final class BlazeAndroidRunConfigurationValidationUtil {
+
+  private static final String SYNC_FAILED_ERR_MSG =
+      "Project state is invalid. Please sync and try your action again.";
+
+  /**
+   * Finds the top error, as determined by {@link ValidationError#compareTo(Object)}. If it is
+   * fatal, it is thrown as a {@link RuntimeConfigurationError}; otherwise it is thrown as a {@link
+   * RuntimeConfigurationWarning}. If no errors exist, nothing is thrown.
+   */
+  public static void throwTopConfigurationError(List<ValidationError> errors)
+      throws RuntimeConfigurationException {
+    if (errors.isEmpty()) {
+      return;
+    }
+    // TODO: Do something with the extra error information? Error count?
+    ValidationError topError = Ordering.natural().max(errors);
+    if (topError.isFatal()) {
+      throw new RuntimeConfigurationError(topError.getMessage(), topError.getQuickfix());
+    }
+    throw new RuntimeConfigurationWarning(topError.getMessage(), topError.getQuickfix());
+  }
+
+  public static List<ValidationError> validateModule(@Nullable Module module) {
+    List<ValidationError> errors = Lists.newArrayList();
+    if (module == null) {
+      errors.add(
+          ValidationError.fatal(
+              "No run configuration module found. Have you successfully synced your project?"));
+      return errors;
+    }
+    final Project project = module.getProject();
+    if (Projects.requiredAndroidModelMissing(project)) {
+      errors.add(ValidationError.fatal(SYNC_FAILED_ERR_MSG));
+    }
+    return errors;
+  }
+
+  public static List<ValidationError> validateFacet(@Nullable AndroidFacet facet, Module module) {
+    List<ValidationError> errors = Lists.newArrayList();
+    if (facet == null) {
+      errors.add(ValidationError.fatal(AndroidBundle.message("no.facet.error", module.getName())));
+      return errors;
+    }
+    if (facet.getConfiguration().getAndroidPlatform() == null) {
+      errors.add(ValidationError.fatal(AndroidBundle.message("select.platform.error")));
+    }
+    return errors;
+  }
+
+  public static List<ValidationError> validateLabel(
+      @Nullable Label label, Project project, Kind kind) {
+    List<ValidationError> errors = Lists.newArrayList();
+    if (label == null) {
+      errors.add(ValidationError.fatal("No target selected."));
+      return errors;
+    }
+    RuleIdeInfo rule = RuleFinder.getInstance().ruleForTarget(project, label);
+    if (rule == null) {
+      errors.add(
+          ValidationError.fatal(
+              String.format("No existing %s rule selected.", Blaze.buildSystemName(project))));
+    } else if (!rule.kindIsOneOf(kind)) {
+      errors.add(
+          ValidationError.fatal(
+              String.format(
+                  "Selected %s rule is not %s", Blaze.buildSystemName(project), kind.toString())));
+    }
+    return errors;
+  }
+
+  public static void validateExecution(
+      @Nullable Module module,
+      @Nullable AndroidFacet facet,
+      @Nullable ProjectViewSet projectViewSet)
+      throws ExecutionException {
+    List<ValidationError> errors = Lists.newArrayList();
+    errors.addAll(validateModule(module));
+    if (module != null) {
+      errors.addAll(validateFacet(facet, module));
+    }
+    if (projectViewSet == null) {
+      errors.add(ValidationError.fatal("Could not load project view. Please resync project"));
+    }
+
+    if (errors.isEmpty()) {
+      return;
+    }
+    ValidationError topError = Ordering.natural().max(errors);
+    if (topError.isFatal()) {
+      throw new ExecutionException(topError.getMessage());
+    }
+  }
+}
diff --git a/aswb/src/com/google/idea/blaze/android/run/binary/BlazeAndroidBinaryProgramRunner.java b/aswb/src/com/google/idea/blaze/android/run/binary/BlazeAndroidBinaryProgramRunner.java
index 5c41e6a..37619aa 100644
--- a/aswb/src/com/google/idea/blaze/android/run/binary/BlazeAndroidBinaryProgramRunner.java
+++ b/aswb/src/com/google/idea/blaze/android/run/binary/BlazeAndroidBinaryProgramRunner.java
@@ -51,7 +51,7 @@
     if (!(handler instanceof BlazeAndroidBinaryRunConfigurationHandler)) {
       return false;
     }
-    return ((BlazeAndroidBinaryRunConfigurationHandler) handler).getConfigState().mobileInstall()
+    return ((BlazeAndroidBinaryRunConfigurationHandler) handler).getState().mobileInstall()
         && (IncrementalInstallDebugExecutor.EXECUTOR_ID.equals(executorId)
             || IncrementalInstallRunExecutor.EXECUTOR_ID.equals(executorId));
   }
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 2bd07cc..a70da60 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
@@ -21,43 +21,34 @@
 import com.android.tools.idea.run.ValidationError;
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.Lists;
-import com.google.common.collect.Ordering;
 import com.google.idea.blaze.android.run.BlazeAndroidRunConfigurationCommonState;
 import com.google.idea.blaze.android.run.BlazeAndroidRunConfigurationHandler;
-import com.google.idea.blaze.android.run.BlazeAndroidRunConfigurationHandlerEditor;
+import com.google.idea.blaze.android.run.BlazeAndroidRunConfigurationValidationUtil;
 import com.google.idea.blaze.android.run.binary.instantrun.BlazeAndroidBinaryInstantRunContext;
 import com.google.idea.blaze.android.run.binary.mobileinstall.BlazeAndroidBinaryMobileInstallRunContext;
 import com.google.idea.blaze.android.run.runner.BlazeAndroidRunConfigurationRunner;
 import com.google.idea.blaze.android.run.runner.BlazeAndroidRunContext;
 import com.google.idea.blaze.android.sync.projectstructure.BlazeAndroidProjectStructureSyncer;
-import com.google.idea.blaze.base.ideinfo.RuleIdeInfo;
 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.projectview.ProjectViewManager;
+import com.google.idea.blaze.base.projectview.ProjectViewSet;
 import com.google.idea.blaze.base.run.BlazeCommandRunConfiguration;
 import com.google.idea.blaze.base.run.BlazeConfigurationNameBuilder;
-import com.google.idea.blaze.base.run.confighandler.BlazeCommandRunConfigurationHandlerEditor;
-import com.google.idea.blaze.base.run.rulefinder.RuleFinder;
+import com.google.idea.blaze.base.run.confighandler.BlazeCommandRunConfigurationRunner;
 import com.google.idea.blaze.base.settings.Blaze;
 import com.intellij.execution.ExecutionException;
 import com.intellij.execution.Executor;
 import com.intellij.execution.configurations.RunConfiguration;
-import com.intellij.execution.configurations.RunProfileState;
-import com.intellij.execution.configurations.RuntimeConfigurationError;
 import com.intellij.execution.configurations.RuntimeConfigurationException;
-import com.intellij.execution.configurations.RuntimeConfigurationWarning;
 import com.intellij.execution.executors.DefaultRunExecutor;
 import com.intellij.execution.runners.ExecutionEnvironment;
-import com.intellij.openapi.diagnostic.Logger;
 import com.intellij.openapi.module.Module;
 import com.intellij.openapi.project.Project;
-import com.intellij.openapi.util.Comparing;
-import com.intellij.openapi.util.InvalidDataException;
-import com.intellij.openapi.util.WriteExternalException;
 import icons.AndroidIcons;
 import java.util.List;
 import javax.swing.Icon;
-import org.jdom.Element;
 import org.jetbrains.android.facet.AndroidFacet;
 import org.jetbrains.annotations.NotNull;
 import org.jetbrains.annotations.Nullable;
@@ -68,25 +59,68 @@
  */
 public class BlazeAndroidBinaryRunConfigurationHandler
     implements BlazeAndroidRunConfigurationHandler {
-  private static final Logger LOG =
-      Logger.getInstance(BlazeAndroidBinaryRunConfigurationHandler.class);
 
   private final BlazeCommandRunConfiguration configuration;
-  private final BlazeAndroidRunConfigurationCommonState commonState;
   private final BlazeAndroidBinaryRunConfigurationState configState;
-  private final BlazeAndroidRunConfigurationRunner runner;
 
   BlazeAndroidBinaryRunConfigurationHandler(BlazeCommandRunConfiguration configuration) {
     this.configuration = configuration;
-    commonState = new BlazeAndroidRunConfigurationCommonState(ImmutableList.of());
-    configState = new BlazeAndroidBinaryRunConfigurationState();
-    runner =
-        new BlazeAndroidRunConfigurationRunner(
-            configuration.getProject(), this, commonState, false, configuration.getUniqueID());
+    configState =
+        new BlazeAndroidBinaryRunConfigurationState(
+            Blaze.buildSystemName(configuration.getProject()));
   }
 
   @Override
-  public BlazeAndroidRunContext createRunContext(
+  public BlazeAndroidBinaryRunConfigurationState getState() {
+    return configState;
+  }
+
+  @Override
+  public BlazeAndroidRunConfigurationCommonState getCommonState() {
+    return configState.getCommonState();
+  }
+
+  @Override
+  @Nullable
+  public Label getLabel() {
+    TargetExpression target = configuration.getTarget();
+    if (target instanceof Label) {
+      return (Label) target;
+    }
+    return null;
+  }
+
+  @Nullable
+  private Module getModule() {
+    Label target = getLabel();
+    return target != null
+        ? BlazeAndroidProjectStructureSyncer.ensureRunConfigurationModule(
+            configuration.getProject(), target)
+        : null;
+  }
+
+  @Override
+  public BlazeCommandRunConfigurationRunner createRunner(
+      Executor executor, ExecutionEnvironment environment) throws ExecutionException {
+    Project project = environment.getProject();
+
+    Module module = getModule();
+    AndroidFacet facet = module != null ? AndroidFacet.getInstance(module) : null;
+    ProjectViewSet projectViewSet = ProjectViewManager.getInstance(project).getProjectViewSet();
+    BlazeAndroidRunConfigurationValidationUtil.validateExecution(module, facet, projectViewSet);
+
+    ImmutableList<String> buildFlags = configState.getBuildFlags(project, projectViewSet);
+    BlazeAndroidRunContext runContext = createRunContext(project, facet, environment, buildFlags);
+
+    return new BlazeAndroidRunConfigurationRunner(
+        module,
+        runContext,
+        getCommonState().getDeployTargetManager(),
+        getCommonState().getDebuggerManager(),
+        configuration.getUniqueID());
+  }
+
+  private BlazeAndroidRunContext createRunContext(
       Project project,
       AndroidFacet facet,
       ExecutionEnvironment env,
@@ -104,120 +138,34 @@
   }
 
   @Override
-  @Nullable
-  public Label getLabel() {
-    TargetExpression target = configuration.getTarget();
-    if (target instanceof Label) {
-      return (Label) target;
-    }
-    return null;
-  }
-
-  @Override
-  public BlazeAndroidRunConfigurationCommonState getCommonState() {
-    return commonState;
-  }
-
-  @Override
-  public BlazeAndroidBinaryRunConfigurationState getConfigState() {
-    return configState;
-  }
-
-  @Nullable
-  private Module getModule() {
-    Label target = getLabel();
-    return target != null
-        ? BlazeAndroidProjectStructureSyncer.ensureRunConfigurationModule(
-            configuration.getProject(), target)
-        : null;
-  }
-
-  @Override
   public final void checkConfiguration() throws RuntimeConfigurationException {
-    List<ValidationError> errors = validate();
-    if (errors.isEmpty()) {
-      return;
-    }
-    // TODO: Do something with the extra error information? Error count?
-    ValidationError topError = Ordering.natural().max(errors);
-    if (topError.isFatal()) {
-      throw new RuntimeConfigurationError(topError.getMessage(), topError.getQuickfix());
-    }
-    throw new RuntimeConfigurationWarning(topError.getMessage(), topError.getQuickfix());
+    BlazeAndroidRunConfigurationValidationUtil.throwTopConfigurationError(validate());
   }
 
+  /**
+   * We collect errors rather than throwing to avoid missing fatal errors by exiting early for a
+   * warning. We use a separate method for the collection so the compiler prevents us from
+   * accidentally throwing.
+   */
   private List<ValidationError> validate() {
     List<ValidationError> errors = Lists.newArrayList();
-    errors.addAll(runner.validate(getModule()));
-    validateLabel(errors);
+    Module module = getModule();
+    errors.addAll(BlazeAndroidRunConfigurationValidationUtil.validateModule(module));
+    AndroidFacet facet = null;
+    if (module != null) {
+      facet = AndroidFacet.getInstance(module);
+      errors.addAll(BlazeAndroidRunConfigurationValidationUtil.validateFacet(facet, module));
+    }
+    errors.addAll(configState.validate(facet));
+    errors.addAll(
+        BlazeAndroidRunConfigurationValidationUtil.validateLabel(
+            getLabel(), configuration.getProject(), Kind.ANDROID_BINARY));
     return errors;
   }
 
-  private void validateLabel(List<ValidationError> errors) {
-    Project project = configuration.getProject();
-    Label target = getLabel();
-    Kind kind = Kind.ANDROID_BINARY;
-    RuleIdeInfo rule =
-        target != null ? RuleFinder.getInstance().ruleForTarget(project, target) : null;
-    if (rule == null) {
-      errors.add(
-          ValidationError.fatal(
-              String.format("No existing %s rule selected.", Blaze.buildSystemName(project))));
-    } else if (!rule.kindIsOneOf(kind)) {
-      errors.add(
-          ValidationError.fatal(
-              String.format(
-                  "Selected %s rule is not %s", Blaze.buildSystemName(project), kind.toString())));
-    }
-  }
-
-  @Override
-  public void readExternal(Element element) throws InvalidDataException {
-    commonState.readExternal(element);
-    runner.readExternal(element);
-    configState.readExternal(element);
-  }
-
-  @Override
-  public void writeExternal(Element element) throws WriteExternalException {
-    commonState.writeExternal(element);
-    runner.writeExternal(element);
-    configState.writeExternal(element);
-  }
-
-  @Override
-  public BlazeAndroidBinaryRunConfigurationHandler cloneFor(
-      BlazeCommandRunConfiguration configuration) {
-    final Element element = new Element("dummy");
-    try {
-      writeExternal(element);
-      final BlazeAndroidBinaryRunConfigurationHandler handler =
-          new BlazeAndroidBinaryRunConfigurationHandler(configuration);
-      handler.readExternal(element);
-      return handler;
-    } catch (InvalidDataException | WriteExternalException e) {
-      LOG.error(e);
-      return null;
-    }
-  }
-
   @Override
   @Nullable
-  public final RunProfileState getState(
-      @NotNull final Executor executor, @NotNull ExecutionEnvironment env)
-      throws ExecutionException {
-    final Module module = getModule();
-    return runner.getState(module, executor, env);
-  }
-
-  @Override
-  public boolean executeBeforeRunTask(ExecutionEnvironment environment) {
-    return runner.executeBuild(environment);
-  }
-
-  @Override
-  @Nullable
-  public String suggestedName() {
+  public String suggestedName(BlazeCommandRunConfiguration configuration) {
     Label target = getLabel();
     if (target == null) {
       return null;
@@ -227,11 +175,6 @@
   }
 
   @Override
-  public boolean isGeneratedName(boolean hasGeneratedFlag) {
-    return Comparing.equal(configuration.getName(), suggestedName());
-  }
-
-  @Override
   @Nullable
   public String getCommandName() {
     return null;
@@ -267,11 +210,4 @@
         ? AndroidIcons.RunIcons.Replay
         : AndroidIcons.RunIcons.DebugReattach;
   }
-
-  @Override
-  public BlazeCommandRunConfigurationHandlerEditor getHandlerEditor() {
-    Project project = configuration.getProject();
-    return new BlazeAndroidRunConfigurationHandlerEditor(
-        project, new BlazeAndroidBinaryRunConfigurationStateEditor(project));
-  }
 }
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 aab1abc..e657124 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
@@ -15,18 +15,26 @@
  */
 package com.google.idea.blaze.android.run.binary;
 
+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.BlazeAndroidRunConfigurationState;
+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;
 import com.intellij.openapi.util.InvalidDataException;
 import com.intellij.openapi.util.WriteExternalException;
+import java.util.List;
 import java.util.Map;
+import javax.annotation.Nullable;
 import org.jdom.Element;
+import org.jetbrains.android.facet.AndroidFacet;
 
 /** State specific to the android binary run configuration. */
-public final class BlazeAndroidBinaryRunConfigurationState
-    implements BlazeAndroidRunConfigurationState {
+public final class BlazeAndroidBinaryRunConfigurationState implements RunConfigurationState {
   public static final String LAUNCH_DEFAULT_ACTIVITY = "default_activity";
   public static final String LAUNCH_SPECIFIC_ACTIVITY = "specific_activity";
   public static final String DO_NOTHING = "do_nothing";
@@ -51,6 +59,16 @@
   private String activityClass = "";
   private String mode = LAUNCH_DEFAULT_ACTIVITY;
 
+  private final BlazeAndroidRunConfigurationCommonState commonState;
+
+  BlazeAndroidBinaryRunConfigurationState(String buildSystemName) {
+    commonState = new BlazeAndroidRunConfigurationCommonState(buildSystemName, false);
+  }
+
+  public BlazeAndroidRunConfigurationCommonState getCommonState() {
+    return commonState;
+  }
+
   boolean mobileInstall() {
     return mobileInstall;
   }
@@ -115,8 +133,22 @@
     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.
+   */
+  public List<ValidationError> validate(@Nullable AndroidFacet facet) {
+    return commonState.validate(facet);
+  }
+
   @Override
   public void readExternal(Element element) throws InvalidDataException {
+    commonState.readExternal(element);
+
     setDeepLink(Strings.nullToEmpty(element.getAttributeValue(DEEP_LINK)));
     setActivityClass(Strings.nullToEmpty(element.getAttributeValue(ACTIVITY_CLASS)));
     setMode(Strings.nullToEmpty(element.getAttributeValue(MODE)));
@@ -156,6 +188,8 @@
 
   @Override
   public void writeExternal(Element element) throws WriteExternalException {
+    commonState.writeExternal(element);
+
     element.setAttribute(DEEP_LINK, deepLink);
     element.setAttribute(ACTIVITY_CLASS, activityClass);
     element.setAttribute(MODE, mode);
@@ -166,6 +200,8 @@
 
     if (userId != null) {
       element.setAttribute(USER_ID_ATTR, Integer.toString(userId));
+    } else {
+      element.removeAttribute(USER_ID_ATTR);
     }
   }
 
@@ -179,4 +215,10 @@
     }
     return result;
   }
+
+  @Override
+  public RunConfigurationStateEditor getEditor(Project project) {
+    return new BlazeAndroidBinaryRunConfigurationStateEditor(
+        commonState.getEditor(project), project);
+  }
 }
diff --git a/aswb/src/com/google/idea/blaze/android/run/binary/BlazeAndroidBinaryRunConfigurationStateEditor.java b/aswb/src/com/google/idea/blaze/android/run/binary/BlazeAndroidBinaryRunConfigurationStateEditor.java
index ddcbf9a..c7fbff6 100644
--- a/aswb/src/com/google/idea/blaze/android/run/binary/BlazeAndroidBinaryRunConfigurationStateEditor.java
+++ b/aswb/src/com/google/idea/blaze/android/run/binary/BlazeAndroidBinaryRunConfigurationStateEditor.java
@@ -16,10 +16,11 @@
 package com.google.idea.blaze.android.run.binary;
 
 import com.android.tools.idea.run.activity.ActivityLocatorUtils;
-import com.google.idea.blaze.android.run.BlazeAndroidRunConfigurationState;
-import com.google.idea.blaze.android.run.BlazeAndroidRunConfigurationStateEditor;
 import com.google.idea.blaze.android.run.binary.instantrun.InstantRunExperiment;
+import com.google.idea.blaze.base.run.state.RunConfigurationState;
+import com.google.idea.blaze.base.run.state.RunConfigurationStateEditor;
 import com.google.idea.blaze.base.ui.IntegerTextField;
+import com.google.idea.blaze.base.ui.UiUtil;
 import com.intellij.codeInsight.daemon.DaemonCodeAnalyzer;
 import com.intellij.ide.util.TreeClassChooser;
 import com.intellij.ide.util.TreeClassChooserFactory;
@@ -40,7 +41,6 @@
 import com.intellij.uiDesigner.core.GridConstraints;
 import com.intellij.uiDesigner.core.GridLayoutManager;
 import java.awt.Color;
-import java.awt.Component;
 import java.awt.Font;
 import java.awt.Insets;
 import java.awt.event.ActionEvent;
@@ -50,26 +50,25 @@
 import javax.swing.BorderFactory;
 import javax.swing.ButtonGroup;
 import javax.swing.JCheckBox;
+import javax.swing.JComponent;
 import javax.swing.JLabel;
 import javax.swing.JPanel;
 import javax.swing.JRadioButton;
 import javax.swing.border.TitledBorder;
 import org.jetbrains.android.util.AndroidBundle;
 import org.jetbrains.android.util.AndroidUtils;
-import org.jetbrains.annotations.Nullable;
 
 /**
  * The part of the Blaze Android Binary handler editor that allows the user to pick an activity to
  * launch. Patterned after {@link org.jetbrains.android.run.ApplicationRunParameters}.
  */
-class BlazeAndroidBinaryRunConfigurationStateEditor
-    implements BlazeAndroidRunConfigurationStateEditor {
+class BlazeAndroidBinaryRunConfigurationStateEditor implements RunConfigurationStateEditor {
   public static final Key<BlazeAndroidBinaryRunConfigurationStateEditor>
       ACTIVITY_CLASS_TEXT_FIELD_KEY = Key.create("BlazeActivityClassTextField");
 
-  private final Project project;
+  private final RunConfigurationStateEditor commonStateEditor;
 
-  @Nullable private JPanel panel;
+  private JPanel panel;
   private ComponentWithBrowseButton<EditorTextField> activityField;
   private JRadioButton launchNothingButton;
   private JRadioButton launchDefaultButton;
@@ -81,10 +80,10 @@
   private JLabel userIdLabel;
   private IntegerTextField userIdField;
 
-  BlazeAndroidBinaryRunConfigurationStateEditor(Project project) {
-    this.project = project;
-
-    setupUI();
+  BlazeAndroidBinaryRunConfigurationStateEditor(
+      RunConfigurationStateEditor commonStateEditor, Project project) {
+    this.commonStateEditor = commonStateEditor;
+    setupUI(project);
     userIdField.setMinValue(0);
 
     activityField.addActionListener(
@@ -134,7 +133,7 @@
 
     instantRunCheckBox.setVisible(InstantRunExperiment.INSTANT_RUN_ENABLED.getValue());
 
-    /** Only one of mobile-install and instant run can be selected at any one time */
+    /* Only one of mobile-install and instant run can be selected at any one time */
     mobileInstallCheckBox.addActionListener(
         e -> {
           if (mobileInstallCheckBox.isSelected()) {
@@ -158,16 +157,13 @@
   }
 
   @Override
-  public void resetEditorFrom(BlazeAndroidRunConfigurationState state) {
-    BlazeAndroidBinaryRunConfigurationState configState =
-        (BlazeAndroidBinaryRunConfigurationState) state;
+  public void resetEditorFrom(RunConfigurationState genericState) {
+    BlazeAndroidBinaryRunConfigurationState state =
+        (BlazeAndroidBinaryRunConfigurationState) genericState;
+    commonStateEditor.resetEditorFrom(state.getCommonState());
     boolean launchSpecificActivity =
-        configState
-            .getMode()
-            .equals(BlazeAndroidBinaryRunConfigurationState.LAUNCH_SPECIFIC_ACTIVITY);
-    if (configState
-        .getMode()
-        .equals(BlazeAndroidBinaryRunConfigurationState.LAUNCH_DEFAULT_ACTIVITY)) {
+        state.getMode().equals(BlazeAndroidBinaryRunConfigurationState.LAUNCH_SPECIFIC_ACTIVITY);
+    if (state.getMode().equals(BlazeAndroidBinaryRunConfigurationState.LAUNCH_DEFAULT_ACTIVITY)) {
       launchDefaultButton.setSelected(true);
     } else if (launchSpecificActivity) {
       launchCustomButton.setSelected(true);
@@ -176,17 +172,17 @@
     }
     activityField.setEnabled(launchSpecificActivity);
     if (launchSpecificActivity) {
-      activityField.getChildComponent().setText(configState.getActivityClass());
+      activityField.getChildComponent().setText(state.getActivityClass());
     }
 
-    mobileInstallCheckBox.setSelected(configState.mobileInstall());
-    splitApksCheckBox.setSelected(configState.useSplitApksIfPossible());
-    instantRunCheckBox.setSelected(configState.instantRun());
-    useWorkProfileIfPresentCheckBox.setSelected(configState.useWorkProfileIfPresent());
+    mobileInstallCheckBox.setSelected(state.mobileInstall());
+    splitApksCheckBox.setSelected(state.useSplitApksIfPossible());
+    instantRunCheckBox.setSelected(state.instantRun());
+    useWorkProfileIfPresentCheckBox.setSelected(state.useWorkProfileIfPresent());
 
-    userIdField.setValue(configState.getUserId());
-    setUserIdEnabled(!configState.useWorkProfileIfPresent());
-    splitApksCheckBox.setVisible(configState.mobileInstall());
+    userIdField.setValue(state.getUserId());
+    setUserIdEnabled(!state.useWorkProfileIfPresent());
+    splitApksCheckBox.setVisible(state.mobileInstall());
   }
 
   private void setUserIdEnabled(boolean enabled) {
@@ -195,30 +191,32 @@
   }
 
   @Override
-  public Component getComponent() {
-    return panel;
+  public void applyEditorTo(RunConfigurationState genericState) {
+    BlazeAndroidBinaryRunConfigurationState state =
+        (BlazeAndroidBinaryRunConfigurationState) genericState;
+    commonStateEditor.applyEditorTo(state.getCommonState());
+
+    state.setUserId((Integer) userIdField.getValue());
+    if (launchDefaultButton.isSelected()) {
+      state.setMode(BlazeAndroidBinaryRunConfigurationState.LAUNCH_DEFAULT_ACTIVITY);
+    } else if (launchCustomButton.isSelected()) {
+      state.setMode(BlazeAndroidBinaryRunConfigurationState.LAUNCH_SPECIFIC_ACTIVITY);
+      state.setActivityClass(activityField.getChildComponent().getText());
+    } else {
+      state.setMode(BlazeAndroidBinaryRunConfigurationState.DO_NOTHING);
+    }
+    state.setMobileInstall(mobileInstallCheckBox.isSelected());
+    state.setUseSplitApksIfPossible(splitApksCheckBox.isSelected());
+    state.setInstantRun(instantRunCheckBox.isSelected());
+    state.setUseWorkProfileIfPresent(useWorkProfileIfPresentCheckBox.isSelected());
   }
 
   @Override
-  public void applyEditorTo(BlazeAndroidRunConfigurationState state) {
-    BlazeAndroidBinaryRunConfigurationState configState =
-        (BlazeAndroidBinaryRunConfigurationState) state;
-    configState.setUserId((Integer) userIdField.getValue());
-    if (launchDefaultButton.isSelected()) {
-      configState.setMode(BlazeAndroidBinaryRunConfigurationState.LAUNCH_DEFAULT_ACTIVITY);
-    } else if (launchCustomButton.isSelected()) {
-      configState.setMode(BlazeAndroidBinaryRunConfigurationState.LAUNCH_SPECIFIC_ACTIVITY);
-      configState.setActivityClass(activityField.getChildComponent().getText());
-    } else {
-      configState.setMode(BlazeAndroidBinaryRunConfigurationState.DO_NOTHING);
-    }
-    configState.setMobileInstall(mobileInstallCheckBox.isSelected());
-    configState.setUseSplitApksIfPossible(splitApksCheckBox.isSelected());
-    configState.setInstantRun(instantRunCheckBox.isSelected());
-    configState.setUseWorkProfileIfPresent(useWorkProfileIfPresentCheckBox.isSelected());
+  public JComponent createComponent() {
+    return UiUtil.createBox(commonStateEditor.createComponent(), panel);
   }
 
-  private void createUIComponents() {
+  private void createUIComponents(Project project) {
     final EditorTextField editorTextField =
         new LanguageTextField(PlainTextLanguage.INSTANCE, project, "") {
           @Override
@@ -239,8 +237,8 @@
   }
 
   /** Initially generated by IntelliJ from a .form file, then checked in as source. */
-  private void setupUI() {
-    createUIComponents();
+  private void setupUI(Project project) {
+    createUIComponents(project);
     panel = new JPanel();
     panel.setLayout(new GridLayoutManager(5, 2, new Insets(0, 0, 0, 0), -1, -1));
     final JPanel activityPanel = new JPanel();
diff --git a/aswb/src/com/google/idea/blaze/android/run/deployinfo/BlazeAndroidDeployInfo.java b/aswb/src/com/google/idea/blaze/android/run/deployinfo/BlazeAndroidDeployInfo.java
index f3ca382..e5f5ad3 100644
--- a/aswb/src/com/google/idea/blaze/android/run/deployinfo/BlazeAndroidDeployInfo.java
+++ b/aswb/src/com/google/idea/blaze/android/run/deployinfo/BlazeAndroidDeployInfo.java
@@ -83,4 +83,13 @@
         .map(artifact -> new File(executionRoot, artifact.getExecRootPath()))
         .collect(Collectors.toList());
   }
+
+  /** Returns the full list of data dependencies to deploy, if any. */
+  public List<File> getDataToDeploy() {
+    return deployInfo
+        .getDataToDeployList()
+        .stream()
+        .map(artifact -> new File(executionRoot, artifact.getExecRootPath()))
+        .collect(Collectors.toList());
+  }
 }
diff --git a/aswb/src/com/google/idea/blaze/android/run/runner/BlazeAndroidDeviceSelector.java b/aswb/src/com/google/idea/blaze/android/run/runner/BlazeAndroidDeviceSelector.java
index a1ef71c..3775550 100644
--- a/aswb/src/com/google/idea/blaze/android/run/runner/BlazeAndroidDeviceSelector.java
+++ b/aswb/src/com/google/idea/blaze/android/run/runner/BlazeAndroidDeviceSelector.java
@@ -117,7 +117,8 @@
         }
       }
 
-      DeployTarget deployTarget = deployTargetManager.getDeployTarget(executor, env, facet);
+      DeployTarget deployTarget =
+          deployTargetManager.getDeployTarget(executor, env, facet, runConfigId);
       if (deployTarget == null) {
         return null;
       }
@@ -138,7 +139,8 @@
       String currentExecutor = executor.getId();
 
       if (ourKillLaunchOption.isToBeShown()) {
-        String msg, noText;
+        String msg;
+        String noText;
         if (previousExecutor.equals(currentExecutor)) {
           msg =
               String.format(
diff --git a/aswb/src/com/google/idea/blaze/android/run/runner/BlazeAndroidLaunchTasksProvider.java b/aswb/src/com/google/idea/blaze/android/run/runner/BlazeAndroidLaunchTasksProvider.java
index 2916688..102512a 100644
--- a/aswb/src/com/google/idea/blaze/android/run/runner/BlazeAndroidLaunchTasksProvider.java
+++ b/aswb/src/com/google/idea/blaze/android/run/runner/BlazeAndroidLaunchTasksProvider.java
@@ -109,7 +109,7 @@
               launchOptions,
               userId,
               debuggerManager.getAndroidDebugger(),
-              debuggerManager.getAndroidDebuggerState(),
+              debuggerManager.getAndroidDebuggerState(project),
               processHandlerLaunchStatus);
       if (appLaunchTask != null) {
         launchTasks.add(appLaunchTask);
@@ -159,7 +159,7 @@
     }
 
     AndroidDebugger androidDebugger = debuggerManager.getAndroidDebugger();
-    AndroidDebuggerState androidDebuggerState = debuggerManager.getAndroidDebuggerState();
+    AndroidDebuggerState androidDebuggerState = debuggerManager.getAndroidDebuggerState(project);
 
     if (androidDebugger == null || androidDebuggerState == null) {
       return null;
diff --git a/aswb/src/com/google/idea/blaze/android/run/runner/BlazeAndroidRunConfigurationDebuggerManager.java b/aswb/src/com/google/idea/blaze/android/run/runner/BlazeAndroidRunConfigurationDebuggerManager.java
index d51f14a..0b429a1 100644
--- a/aswb/src/com/google/idea/blaze/android/run/runner/BlazeAndroidRunConfigurationDebuggerManager.java
+++ b/aswb/src/com/google/idea/blaze/android/run/runner/BlazeAndroidRunConfigurationDebuggerManager.java
@@ -38,20 +38,18 @@
 
 /** Manages android debugger state for the run configurations. */
 public class BlazeAndroidRunConfigurationDebuggerManager implements JDOMExternalizable {
-  private final Project project;
   private final Map<String, AndroidDebuggerState> androidDebuggerStates = Maps.newHashMap();
   private final BlazeAndroidRunConfigurationCommonState commonState;
 
-  BlazeAndroidRunConfigurationDebuggerManager(
-      Project project, BlazeAndroidRunConfigurationCommonState commonState) {
-    this.project = project;
+  public BlazeAndroidRunConfigurationDebuggerManager(
+      BlazeAndroidRunConfigurationCommonState commonState) {
     this.commonState = commonState;
     for (AndroidDebugger androidDebugger : getAndroidDebuggers()) {
       this.androidDebuggerStates.put(androidDebugger.getId(), androidDebugger.createState());
     }
   }
 
-  List<ValidationError> validate(AndroidFacet facet) {
+  public List<ValidationError> validate(AndroidFacet facet) {
     // All of the AndroidDebuggerState classes implement a validate that
     // either does nothing or is specific to gradle so there is no point
     // in calling validate on our AndroidDebuggerState.
@@ -70,7 +68,7 @@
   }
 
   @Nullable
-  final <T extends AndroidDebuggerState> T getAndroidDebuggerState() {
+  final <T extends AndroidDebuggerState> T getAndroidDebuggerState(Project project) {
     T androidDebuggerState = getAndroidDebuggerState(getDebuggerID());
     // Set our working directory to our workspace root for native debugging.
     if (androidDebuggerState instanceof NativeAndroidDebuggerState) {
diff --git a/aswb/src/com/google/idea/blaze/android/run/runner/BlazeAndroidRunConfigurationDeployTargetManager.java b/aswb/src/com/google/idea/blaze/android/run/runner/BlazeAndroidRunConfigurationDeployTargetManager.java
index 306bec4..44a8eb3 100644
--- a/aswb/src/com/google/idea/blaze/android/run/runner/BlazeAndroidRunConfigurationDeployTargetManager.java
+++ b/aswb/src/com/google/idea/blaze/android/run/runner/BlazeAndroidRunConfigurationDeployTargetManager.java
@@ -40,13 +40,11 @@
 public class BlazeAndroidRunConfigurationDeployTargetManager implements JDOMExternalizable {
   private static final String TARGET_SELECTION_MODE = TargetSelectionMode.SHOW_DIALOG.name();
 
-  private final int runConfigId;
   private final boolean isAndroidTest;
   private final List<DeployTargetProvider> deployTargetProviders;
   private final Map<String, DeployTargetState> deployTargetStates;
 
-  BlazeAndroidRunConfigurationDeployTargetManager(int runConfigId, boolean isAndroidTest) {
-    this.runConfigId = runConfigId;
+  public BlazeAndroidRunConfigurationDeployTargetManager(boolean isAndroidTest) {
     this.isAndroidTest = isAndroidTest;
     this.deployTargetProviders = DeployTargetProvider.getProviders();
 
@@ -57,12 +55,13 @@
     this.deployTargetStates = builder.build();
   }
 
-  List<ValidationError> validate(AndroidFacet facet) {
+  public List<ValidationError> validate(AndroidFacet facet) {
     return getCurrentDeployTargetState().validate(facet);
   }
 
   @Nullable
-  DeployTarget getDeployTarget(Executor executor, ExecutionEnvironment env, AndroidFacet facet)
+  DeployTarget getDeployTarget(
+      Executor executor, ExecutionEnvironment env, AndroidFacet facet, int runConfigId)
       throws ExecutionException {
     DeployTargetProvider currentTargetProvider = getCurrentDeployTargetProvider();
 
diff --git a/aswb/src/com/google/idea/blaze/android/run/runner/BlazeAndroidRunConfigurationRunner.java b/aswb/src/com/google/idea/blaze/android/run/runner/BlazeAndroidRunConfigurationRunner.java
index e0cef2a..76e3bfc 100644
--- a/aswb/src/com/google/idea/blaze/android/run/runner/BlazeAndroidRunConfigurationRunner.java
+++ b/aswb/src/com/google/idea/blaze/android/run/runner/BlazeAndroidRunConfigurationRunner.java
@@ -16,7 +16,6 @@
 
 package com.google.idea.blaze.android.run.runner;
 
-import static com.android.tools.idea.gradle.util.Projects.requiredAndroidModelMissing;
 import static org.jetbrains.android.actions.RunAndroidAvdManagerAction.getName;
 
 import com.android.ddmlib.IDevice;
@@ -28,22 +27,15 @@
 import com.android.tools.idea.run.LaunchInfo;
 import com.android.tools.idea.run.LaunchOptions;
 import com.android.tools.idea.run.LaunchTaskRunner;
-import com.android.tools.idea.run.ValidationError;
 import com.android.tools.idea.run.editor.DeployTarget;
 import com.android.tools.idea.run.editor.DeployTargetState;
 import com.android.tools.idea.run.tasks.LaunchTasksProvider;
 import com.android.tools.idea.run.util.LaunchUtils;
-import com.google.common.collect.ImmutableList;
-import com.google.common.collect.Lists;
 import com.google.common.util.concurrent.Futures;
 import com.google.common.util.concurrent.ListenableFuture;
-import com.google.idea.blaze.android.run.BlazeAndroidRunConfigurationCommonState;
-import com.google.idea.blaze.android.run.BlazeAndroidRunConfigurationHandler;
-import com.google.idea.blaze.base.command.BlazeFlags;
 import com.google.idea.blaze.base.experiments.ExperimentScope;
 import com.google.idea.blaze.base.metrics.Action;
-import com.google.idea.blaze.base.projectview.ProjectViewManager;
-import com.google.idea.blaze.base.projectview.ProjectViewSet;
+import com.google.idea.blaze.base.run.confighandler.BlazeCommandRunConfigurationRunner;
 import com.google.idea.blaze.base.scope.Scope;
 import com.google.idea.blaze.base.scope.output.IssueOutput;
 import com.google.idea.blaze.base.scope.scopes.BlazeConsoleScope;
@@ -65,12 +57,7 @@
 import com.intellij.openapi.module.Module;
 import com.intellij.openapi.progress.ProgressManager;
 import com.intellij.openapi.project.Project;
-import com.intellij.openapi.util.InvalidDataException;
-import com.intellij.openapi.util.JDOMExternalizable;
 import com.intellij.openapi.util.Key;
-import com.intellij.openapi.util.WriteExternalException;
-import java.util.List;
-import org.jdom.Element;
 import org.jetbrains.android.facet.AndroidFacet;
 import org.jetbrains.android.sdk.AndroidSdkUtils;
 import org.jetbrains.android.util.AndroidBundle;
@@ -78,128 +65,50 @@
 import org.jetbrains.annotations.Nullable;
 
 /**
- * Supports the entire run configuration flow. Used by both android_binary and android_test.
+ * Supports the execution. Used by both android_binary and android_test.
  *
- * <p>Does any verification necessary, builds the APK and installs it, launches and debug tasks,
- * etc.
+ * <p>Builds the APK and installs it, launches and debug tasks, etc.
  *
  * <p>Any indirection between android_binary/android_test, mobile-install, InstantRun etc. should
  * come via the strategy class.
  */
-public final class BlazeAndroidRunConfigurationRunner implements JDOMExternalizable {
+public final class BlazeAndroidRunConfigurationRunner
+    implements BlazeCommandRunConfigurationRunner {
 
   private static final Logger LOG = Logger.getInstance(BlazeAndroidRunConfigurationRunner.class);
 
-  private static final String SYNC_FAILED_ERR_MSG =
-      "Project state is invalid. Please sync and try your action again.";
-
-  public static final Key<BlazeAndroidRunContext> RUN_CONTEXT_KEY = Key.create("blaze.run.context");
-  public static final Key<BlazeAndroidDeviceSelector.DeviceSession> DEVICE_SESSION_KEY =
+  private static final Key<BlazeAndroidRunContext> RUN_CONTEXT_KEY =
+      Key.create("blaze.run.context");
+  private static final Key<BlazeAndroidDeviceSelector.DeviceSession> DEVICE_SESSION_KEY =
       Key.create("blaze.device.session");
 
-  // We need to split "-c dbg" into two flags because we pass flags
-  // as a list of strings to the command line executor and we need blaze
-  // to see -c and dbg as two separate entities, not one.
-  private static final ImmutableList<String> NATIVE_DEBUG_FLAGS =
-      ImmutableList.of("--fission=no", "-c", "dbg");
-
-  private final Project project;
-
-  private final BlazeAndroidRunConfigurationHandler runConfigurationHandler;
-
-  private final BlazeAndroidRunConfigurationCommonState commonState;
-
+  private final Module module;
+  private final BlazeAndroidRunContext runContext;
+  private final BlazeAndroidRunConfigurationDeployTargetManager deployTargetManager;
+  private final BlazeAndroidRunConfigurationDebuggerManager debuggerManager;
   private final int runConfigId;
 
-  private final BlazeAndroidRunConfigurationDeployTargetManager deployTargetManager;
-
-  private final BlazeAndroidRunConfigurationDebuggerManager debuggerManager;
-
   public BlazeAndroidRunConfigurationRunner(
-      Project project,
-      BlazeAndroidRunConfigurationHandler runConfigurationHandler,
-      BlazeAndroidRunConfigurationCommonState commonState,
-      boolean isAndroidTest,
+      Module module,
+      BlazeAndroidRunContext runContext,
+      BlazeAndroidRunConfigurationDeployTargetManager deployTargetManager,
+      BlazeAndroidRunConfigurationDebuggerManager debuggerManager,
       int runConfigId) {
-    this.project = project;
-    this.runConfigurationHandler = runConfigurationHandler;
-    this.commonState = commonState;
+    this.module = module;
+    this.runContext = runContext;
+    this.deployTargetManager = deployTargetManager;
+    this.debuggerManager = debuggerManager;
     this.runConfigId = runConfigId;
-    this.deployTargetManager =
-        new BlazeAndroidRunConfigurationDeployTargetManager(runConfigId, isAndroidTest);
-    this.debuggerManager = new BlazeAndroidRunConfigurationDebuggerManager(project, commonState);
   }
 
-  private ImmutableList<String> getBuildFlags(Project project, ProjectViewSet projectViewSet) {
-    return ImmutableList.<String>builder()
-        .addAll(BlazeFlags.buildFlags(project, projectViewSet))
-        .addAll(commonState.getUserFlags())
-        .addAll(getNativeDebuggerFlags())
-        .build();
-  }
-
-  public ImmutableList<String> getNativeDebuggerFlags() {
-    return commonState.isNativeDebuggingEnabled() ? NATIVE_DEBUG_FLAGS : ImmutableList.of();
-  }
-
-  /**
-   * We collect errors rather than throwing to avoid missing fatal errors by exiting early for a
-   * warning. We use a separate method for the collection so the compiler prevents us from
-   * accidentally throwing.
-   */
-  public List<ValidationError> validate(@Nullable Module module) {
-    List<ValidationError> errors = Lists.newArrayList();
-    if (module == null) {
-      errors.add(
-          ValidationError.fatal(
-              "No run configuration module found. Have you successfully synced your project?"));
-      return errors;
-    }
-
-    if (runConfigurationHandler.getLabel() == null) {
-      errors.add(ValidationError.fatal("No target selected"));
-      return errors;
-    }
-
-    final Project project = module.getProject();
-    if (requiredAndroidModelMissing(project)) {
-      errors.add(ValidationError.fatal(SYNC_FAILED_ERR_MSG));
-    }
-
-    AndroidFacet facet = AndroidFacet.getInstance(module);
-    if (facet == null) {
-      // Can't proceed.
-      return ImmutableList.of(
-          ValidationError.fatal(AndroidBundle.message("no.facet.error", module.getName())));
-    }
-
-    if (facet.getConfiguration().getAndroidPlatform() == null) {
-      errors.add(ValidationError.fatal(AndroidBundle.message("select.platform.error")));
-    }
-
-    errors.addAll(deployTargetManager.validate(facet));
-    errors.addAll(debuggerManager.validate(facet));
-
-    return errors;
-  }
-
+  @Override
   @Nullable
-  public final RunProfileState getState(
-      Module module, final Executor executor, ExecutionEnvironment env) throws ExecutionException {
+  public final RunProfileState getRunProfileState(final Executor executor, ExecutionEnvironment env)
+      throws ExecutionException {
 
-    assert module != null : "Enforced by fatal validation check in checkConfiguration.";
     final AndroidFacet facet = AndroidFacet.getInstance(module);
-    assert facet != null : "Enforced by fatal validation check in checkConfiguration.";
-    Project project = env.getProject();
-
-    ProjectViewSet projectViewSet = ProjectViewManager.getInstance(project).getProjectViewSet();
-    if (projectViewSet == null) {
-      throw new ExecutionException("Could not load project view. Please resync project");
-    }
-    ImmutableList<String> buildFlags = getBuildFlags(project, projectViewSet);
-
-    BlazeAndroidRunContext runContext =
-        runConfigurationHandler.createRunContext(project, facet, env, buildFlags);
+    assert facet != null : "Enforced by fatal validation check in createRunner.";
+    final Project project = env.getProject();
 
     runContext.augmentEnvironment(env);
 
@@ -279,7 +188,9 @@
         .setForceStopRunningApp(true);
   }
 
-  public boolean executeBuild(ExecutionEnvironment env) {
+  @Override
+  public boolean executeBeforeRunTask(ExecutionEnvironment env) {
+    final Project project = env.getProject();
     boolean suppressConsole = BlazeUserSettings.getInstance().getSuppressConsoleForRunAction();
     return Scope.root(
         context -> {
@@ -400,16 +311,4 @@
       return console == null ? null : new DefaultExecutionResult(console, processHandler);
     }
   }
-
-  @Override
-  public void readExternal(Element element) throws InvalidDataException {
-    deployTargetManager.readExternal(element);
-    debuggerManager.readExternal(element);
-  }
-
-  @Override
-  public void writeExternal(Element element) throws WriteExternalException {
-    deployTargetManager.writeExternal(element);
-    debuggerManager.writeExternal(element);
-  }
 }
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 ff1e6b7..f48fd2c 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
@@ -79,12 +79,11 @@
       return false;
     }
     configuration.setTarget(rule.label);
-    BlazeAndroidTestRunConfigurationHandler handler =
-        configuration.getHandlerIfType(BlazeAndroidTestRunConfigurationHandler.class);
-    if (handler == null) {
+    BlazeAndroidTestRunConfigurationState configState =
+        configuration.getHandlerStateIfType(BlazeAndroidTestRunConfigurationState.class);
+    if (configState == null) {
       return false;
     }
-    BlazeAndroidTestRunConfigurationState configState = handler.getConfigState();
     configState.setTestingType(AndroidTestRunConfiguration.TEST_CLASS);
     configState.setClassName(testClass.getQualifiedName());
     configuration.setGeneratedName();
@@ -122,12 +121,11 @@
 
   private static boolean checkIfAttributesAreTheSame(
       BlazeCommandRunConfiguration configuration, PsiClass testClass) {
-    BlazeAndroidTestRunConfigurationHandler handler =
-        configuration.getHandlerIfType(BlazeAndroidTestRunConfigurationHandler.class);
-    if (handler == null) {
+    BlazeAndroidTestRunConfigurationState configState =
+        configuration.getHandlerStateIfType(BlazeAndroidTestRunConfigurationState.class);
+    if (configState == null) {
       return false;
     }
-    BlazeAndroidTestRunConfigurationState configState = handler.getConfigState();
     if (Strings.isNullOrEmpty(configState.getClassName())) {
       return false;
     }
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 7f87f1b..4334cd4 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
@@ -79,12 +79,11 @@
       return false;
     }
     configuration.setTarget(rule.label);
-    BlazeAndroidTestRunConfigurationHandler handler =
-        configuration.getHandlerIfType(BlazeAndroidTestRunConfigurationHandler.class);
-    if (handler == null) {
+    BlazeAndroidTestRunConfigurationState configState =
+        configuration.getHandlerStateIfType(BlazeAndroidTestRunConfigurationState.class);
+    if (configState == null) {
       return false;
     }
-    BlazeAndroidTestRunConfigurationState configState = handler.getConfigState();
     configState.setTestingType(AndroidTestRunConfiguration.TEST_METHOD);
     configState.setClassName(containingClass.getQualifiedName());
     configState.setMethodName(psiMethod.getName());
@@ -120,12 +119,11 @@
 
   private static boolean checkIfAttributesAreTheSame(
       BlazeCommandRunConfiguration configuration, PsiMethod testMethod) {
-    BlazeAndroidTestRunConfigurationHandler handler =
-        configuration.getHandlerIfType(BlazeAndroidTestRunConfigurationHandler.class);
-    if (handler == null) {
+    BlazeAndroidTestRunConfigurationState configState =
+        configuration.getHandlerStateIfType(BlazeAndroidTestRunConfigurationState.class);
+    if (configState == null) {
       return false;
     }
-    BlazeAndroidTestRunConfigurationState configState = handler.getConfigState();
     if (Strings.isNullOrEmpty(configState.getClassName())
         || Strings.isNullOrEmpty(configState.getMethodName())) {
       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 885a8ae..d63ea0c 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
@@ -18,42 +18,32 @@
 import com.android.tools.idea.run.ValidationError;
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.Lists;
-import com.google.common.collect.Ordering;
 import com.google.idea.blaze.android.run.BlazeAndroidRunConfigurationCommonState;
 import com.google.idea.blaze.android.run.BlazeAndroidRunConfigurationHandler;
-import com.google.idea.blaze.android.run.BlazeAndroidRunConfigurationHandlerEditor;
+import com.google.idea.blaze.android.run.BlazeAndroidRunConfigurationValidationUtil;
 import com.google.idea.blaze.android.run.runner.BlazeAndroidRunConfigurationRunner;
 import com.google.idea.blaze.android.run.runner.BlazeAndroidRunContext;
 import com.google.idea.blaze.android.sync.projectstructure.BlazeAndroidProjectStructureSyncer;
-import com.google.idea.blaze.base.ideinfo.RuleIdeInfo;
 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.projectview.ProjectViewManager;
+import com.google.idea.blaze.base.projectview.ProjectViewSet;
 import com.google.idea.blaze.base.run.BlazeCommandRunConfiguration;
 import com.google.idea.blaze.base.run.BlazeConfigurationNameBuilder;
-import com.google.idea.blaze.base.run.confighandler.BlazeCommandRunConfigurationHandlerEditor;
-import com.google.idea.blaze.base.run.rulefinder.RuleFinder;
+import com.google.idea.blaze.base.run.confighandler.BlazeCommandRunConfigurationRunner;
 import com.google.idea.blaze.base.settings.Blaze;
 import com.intellij.execution.ExecutionException;
 import com.intellij.execution.Executor;
 import com.intellij.execution.JavaExecutionUtil;
 import com.intellij.execution.configurations.RunConfiguration;
-import com.intellij.execution.configurations.RunProfileState;
-import com.intellij.execution.configurations.RuntimeConfigurationError;
 import com.intellij.execution.configurations.RuntimeConfigurationException;
-import com.intellij.execution.configurations.RuntimeConfigurationWarning;
 import com.intellij.execution.runners.ExecutionEnvironment;
-import com.intellij.openapi.diagnostic.Logger;
 import com.intellij.openapi.module.Module;
 import com.intellij.openapi.project.Project;
-import com.intellij.openapi.util.Comparing;
-import com.intellij.openapi.util.InvalidDataException;
-import com.intellij.openapi.util.WriteExternalException;
 import java.util.List;
 import javax.swing.Icon;
-import org.jdom.Element;
 import org.jetbrains.android.facet.AndroidFacet;
-import org.jetbrains.annotations.NotNull;
 import org.jetbrains.annotations.Nullable;
 
 /**
@@ -62,31 +52,25 @@
  */
 public class BlazeAndroidTestRunConfigurationHandler
     implements BlazeAndroidRunConfigurationHandler {
-  private static final Logger LOG =
-      Logger.getInstance(BlazeAndroidTestRunConfigurationHandler.class);
 
   private final BlazeCommandRunConfiguration configuration;
-  private final BlazeAndroidRunConfigurationCommonState commonState;
   private final BlazeAndroidTestRunConfigurationState configState;
-  private final BlazeAndroidRunConfigurationRunner runner;
 
   BlazeAndroidTestRunConfigurationHandler(BlazeCommandRunConfiguration configuration) {
     this.configuration = configuration;
-    commonState = new BlazeAndroidRunConfigurationCommonState(ImmutableList.of());
-    configState = new BlazeAndroidTestRunConfigurationState();
-    runner =
-        new BlazeAndroidRunConfigurationRunner(
-            configuration.getProject(), this, commonState, true, configuration.getUniqueID());
+    configState =
+        new BlazeAndroidTestRunConfigurationState(
+            Blaze.buildSystemName(configuration.getProject()));
   }
 
   @Override
-  public BlazeAndroidRunContext createRunContext(
-      Project project,
-      AndroidFacet facet,
-      ExecutionEnvironment env,
-      ImmutableList<String> buildFlags) {
-    return new BlazeAndroidTestRunContext(
-        project, facet, configuration, env, configState, getLabel(), buildFlags);
+  public BlazeAndroidTestRunConfigurationState getState() {
+    return configState;
+  }
+
+  @Override
+  public BlazeAndroidRunConfigurationCommonState getCommonState() {
+    return configState.getCommonState();
   }
 
   @Override
@@ -99,16 +83,6 @@
     return null;
   }
 
-  @Override
-  public BlazeAndroidRunConfigurationCommonState getCommonState() {
-    return commonState;
-  }
-
-  @Override
-  public BlazeAndroidTestRunConfigurationState getConfigState() {
-    return configState;
-  }
-
   @Nullable
   private Module getModule() {
     Label target = getLabel();
@@ -119,96 +93,70 @@
   }
 
   @Override
-  public final void checkConfiguration() throws RuntimeConfigurationException {
-    List<ValidationError> errors = validate();
-    if (errors.isEmpty()) {
-      return;
-    }
-    // TODO: Do something with the extra error information? Error count?
-    ValidationError topError = Ordering.natural().max(errors);
-    if (topError.isFatal()) {
-      throw new RuntimeConfigurationError(topError.getMessage(), topError.getQuickfix());
-    }
-    throw new RuntimeConfigurationWarning(topError.getMessage(), topError.getQuickfix());
+  public BlazeCommandRunConfigurationRunner createRunner(
+      Executor executor, ExecutionEnvironment environment) throws ExecutionException {
+    Project project = environment.getProject();
+
+    Module module = getModule();
+    AndroidFacet facet = module != null ? AndroidFacet.getInstance(module) : null;
+    ProjectViewSet projectViewSet = ProjectViewManager.getInstance(project).getProjectViewSet();
+    BlazeAndroidRunConfigurationValidationUtil.validateExecution(module, facet, projectViewSet);
+
+    ImmutableList<String> buildFlags = configState.getBuildFlags(project, projectViewSet);
+    BlazeAndroidRunContext runContext = createRunContext(project, facet, environment, buildFlags);
+
+    return new BlazeAndroidRunConfigurationRunner(
+        module,
+        runContext,
+        getCommonState().getDeployTargetManager(),
+        getCommonState().getDebuggerManager(),
+        configuration.getUniqueID());
   }
 
+  private BlazeAndroidRunContext createRunContext(
+      Project project,
+      AndroidFacet facet,
+      ExecutionEnvironment env,
+      ImmutableList<String> buildFlags) {
+    return new BlazeAndroidTestRunContext(
+        project, facet, configuration, env, configState, getLabel(), buildFlags);
+  }
+
+  @Override
+  public final void checkConfiguration() throws RuntimeConfigurationException {
+    BlazeAndroidRunConfigurationValidationUtil.throwTopConfigurationError(validate());
+  }
+
+  /**
+   * We collect errors rather than throwing to avoid missing fatal errors by exiting early for a
+   * warning. We use a separate method for the collection so the compiler prevents us from
+   * accidentally throwing.
+   */
   private List<ValidationError> validate() {
     List<ValidationError> errors = Lists.newArrayList();
-    errors.addAll(runner.validate(getModule()));
-    validateLabel(errors);
+    Module module = getModule();
+    errors.addAll(BlazeAndroidRunConfigurationValidationUtil.validateModule(module));
+    AndroidFacet facet = null;
+    if (module != null) {
+      facet = AndroidFacet.getInstance(module);
+      errors.addAll(BlazeAndroidRunConfigurationValidationUtil.validateFacet(facet, module));
+    }
+    errors.addAll(configState.validate(facet));
+    errors.addAll(
+        BlazeAndroidRunConfigurationValidationUtil.validateLabel(
+            getLabel(), configuration.getProject(), Kind.ANDROID_TEST));
     return errors;
   }
 
-  private void validateLabel(List<ValidationError> errors) {
-    Project project = configuration.getProject();
-    Label target = getLabel();
-    Kind kind = Kind.ANDROID_TEST;
-    RuleIdeInfo rule =
-        target != null ? RuleFinder.getInstance().ruleForTarget(project, target) : null;
-    if (rule == null) {
-      errors.add(
-          ValidationError.fatal(
-              String.format("No existing %s rule selected.", Blaze.buildSystemName(project))));
-    } else if (!rule.kindIsOneOf(kind)) {
-      errors.add(
-          ValidationError.fatal(
-              String.format(
-                  "Selected %s rule is not %s", Blaze.buildSystemName(project), kind.toString())));
-    }
-  }
-
-  @Override
-  public void readExternal(Element element) throws InvalidDataException {
-    commonState.readExternal(element);
-    runner.readExternal(element);
-    configState.readExternal(element);
-  }
-
-  @Override
-  public void writeExternal(Element element) throws WriteExternalException {
-    commonState.writeExternal(element);
-    runner.writeExternal(element);
-    configState.writeExternal(element);
-  }
-
-  @Override
-  public BlazeAndroidTestRunConfigurationHandler cloneFor(
-      BlazeCommandRunConfiguration configuration) {
-    final Element element = new Element("dummy");
-    try {
-      writeExternal(element);
-      final BlazeAndroidTestRunConfigurationHandler handler =
-          new BlazeAndroidTestRunConfigurationHandler(configuration);
-      handler.readExternal(element);
-      return handler;
-    } catch (InvalidDataException | WriteExternalException e) {
-      LOG.error(e);
-      return null;
-    }
-  }
-
   @Override
   @Nullable
-  public final RunProfileState getState(
-      @NotNull final Executor executor, @NotNull ExecutionEnvironment env)
-      throws ExecutionException {
-    final Module module = getModule();
-    return runner.getState(module, executor, env);
-  }
-
-  @Override
-  public boolean executeBeforeRunTask(ExecutionEnvironment environment) {
-    return runner.executeBuild(environment);
-  }
-
-  @Override
-  @Nullable
-  public String suggestedName() {
+  public String suggestedName(BlazeCommandRunConfiguration configuration) {
     Label target = getLabel();
     if (target == null) {
       return null;
     }
-    BlazeConfigurationNameBuilder nameBuilder = new BlazeConfigurationNameBuilder(configuration);
+    BlazeConfigurationNameBuilder nameBuilder =
+        new BlazeConfigurationNameBuilder(this.configuration);
 
     boolean isClassTest =
         configState.getTestingType() == BlazeAndroidTestRunConfigurationState.TEST_CLASS;
@@ -229,22 +177,6 @@
   }
 
   @Override
-  public boolean isGeneratedName(boolean hasGeneratedFlag) {
-    final String name = configuration.getName();
-
-    if ((configState.getTestingType() == BlazeAndroidTestRunConfigurationState.TEST_CLASS
-            || configState.getTestingType() == BlazeAndroidTestRunConfigurationState.TEST_METHOD)
-        && (configState.getClassName() == null || configState.getClassName().length() == 0)) {
-      return JavaExecutionUtil.isNewName(name);
-    }
-    if (configState.getTestingType() == BlazeAndroidTestRunConfigurationState.TEST_METHOD
-        && (configState.getMethodName() == null || configState.getMethodName().length() == 0)) {
-      return JavaExecutionUtil.isNewName(name);
-    }
-    return Comparing.equal(name, suggestedName());
-  }
-
-  @Override
   @Nullable
   public String getCommandName() {
     return "test";
@@ -260,11 +192,4 @@
   public Icon getExecutorIcon(RunConfiguration configuration, Executor executor) {
     return null;
   }
-
-  @Override
-  public BlazeCommandRunConfigurationHandlerEditor getHandlerEditor() {
-    Project project = configuration.getProject();
-    return new BlazeAndroidRunConfigurationHandlerEditor(
-        project, new BlazeAndroidTestRunConfigurationStateEditor(project));
-  }
 }
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 9f05bc5..62fcbb3 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
@@ -15,17 +15,26 @@
  */
 package com.google.idea.blaze.android.run.test;
 
+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.BlazeAndroidRunConfigurationState;
+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;
 import com.intellij.openapi.util.InvalidDataException;
 import com.intellij.openapi.util.WriteExternalException;
+import java.util.List;
 import java.util.Map;
+import javax.annotation.Nullable;
 import org.jdom.Element;
+import org.jetbrains.android.facet.AndroidFacet;
 import org.jetbrains.annotations.Contract;
 
 /** State specific for the android test configuration. */
-final class BlazeAndroidTestRunConfigurationState implements BlazeAndroidRunConfigurationState {
+final class BlazeAndroidTestRunConfigurationState implements RunConfigurationState {
 
   private static final String RUN_THROUGH_BLAZE_ATTR = "blaze-run-through-blaze";
 
@@ -47,7 +56,7 @@
   private static final String EXTRA_OPTIONS = "EXTRA_OPTIONS";
 
   private int testingType = TEST_ALL_IN_MODULE;
-  private String instrumentationRunnerClass;
+  private String instrumentationRunnerClass = "";
   private String methodName = "";
   private String className = "";
   private String packageName = "";
@@ -56,10 +65,14 @@
   // Whether to delegate to 'blaze test'.
   private boolean runThroughBlaze;
 
-  public BlazeAndroidTestRunConfigurationState() {
-    String defaultInstrumentationRunnerClass =
-        InstrumentationRunnerProvider.getDefaultInstrumentationRunnerClass();
-    instrumentationRunnerClass = Strings.nullToEmpty(defaultInstrumentationRunnerClass);
+  private final BlazeAndroidRunConfigurationCommonState commonState;
+
+  public BlazeAndroidTestRunConfigurationState(String buildSystemName) {
+    commonState = new BlazeAndroidRunConfigurationCommonState(buildSystemName, true);
+  }
+
+  public BlazeAndroidRunConfigurationCommonState getCommonState() {
+    return commonState;
   }
 
   @Contract(pure = true)
@@ -119,8 +132,22 @@
     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.
+   */
+  public List<ValidationError> validate(@Nullable AndroidFacet facet) {
+    return commonState.validate(facet);
+  }
+
   @Override
   public void readExternal(Element element) throws InvalidDataException {
+    commonState.readExternal(element);
+
     String testingTypeAttribute = element.getAttributeValue(TESTING_TYPE);
     if (!Strings.isNullOrEmpty(testingTypeAttribute)) {
       testingType = Integer.parseInt(testingTypeAttribute);
@@ -164,6 +191,8 @@
 
   @Override
   public void writeExternal(Element element) throws WriteExternalException {
+    commonState.writeExternal(element);
+
     element.setAttribute(RUN_THROUGH_BLAZE_ATTR, Boolean.toString(runThroughBlaze));
     element.setAttribute(TESTING_TYPE, Integer.toString(testingType));
     element.setAttribute(INSTRUMENTATION_RUNNER_CLASS, instrumentationRunnerClass);
@@ -183,4 +212,9 @@
     }
     return result;
   }
+
+  @Override
+  public RunConfigurationStateEditor getEditor(Project project) {
+    return new BlazeAndroidTestRunConfigurationStateEditor(commonState.getEditor(project), project);
+  }
 }
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 d03f883..eb8293a 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
@@ -21,9 +21,10 @@
 import static com.android.tools.idea.run.testing.AndroidTestRunConfiguration.TEST_CLASS;
 import static com.android.tools.idea.run.testing.AndroidTestRunConfiguration.TEST_METHOD;
 
-import com.google.idea.blaze.android.run.BlazeAndroidRunConfigurationState;
-import com.google.idea.blaze.android.run.BlazeAndroidRunConfigurationStateEditor;
+import com.google.idea.blaze.base.run.state.RunConfigurationState;
+import com.google.idea.blaze.base.run.state.RunConfigurationStateEditor;
 import com.google.idea.blaze.base.settings.Blaze;
+import com.google.idea.blaze.base.ui.UiUtil;
 import com.intellij.openapi.project.Project;
 import com.intellij.openapi.ui.LabeledComponent;
 import com.intellij.ui.EditorTextField;
@@ -31,7 +32,6 @@
 import com.intellij.uiDesigner.core.GridConstraints;
 import com.intellij.uiDesigner.core.GridLayoutManager;
 import com.intellij.uiDesigner.core.Spacer;
-import java.awt.Component;
 import java.awt.Insets;
 import java.awt.event.ActionEvent;
 import java.awt.event.ActionListener;
@@ -39,6 +39,7 @@
 import javax.swing.AbstractButton;
 import javax.swing.ButtonGroup;
 import javax.swing.JCheckBox;
+import javax.swing.JComponent;
 import javax.swing.JLabel;
 import javax.swing.JPanel;
 import javax.swing.JRadioButton;
@@ -47,8 +48,9 @@
  * The part of the Blaze Android Test handler editor that allows the user to pick test filters.
  * Forked from {@link org.jetbrains.android.run.testing.TestRunParameters}.
  */
-class BlazeAndroidTestRunConfigurationStateEditor
-    implements BlazeAndroidRunConfigurationStateEditor {
+class BlazeAndroidTestRunConfigurationStateEditor implements RunConfigurationStateEditor {
+  private final RunConfigurationStateEditor commonStateEditor;
+
   private JRadioButton allInPackageButton;
   private JRadioButton classButton;
   private JRadioButton testMethodButton;
@@ -62,7 +64,9 @@
   private JCheckBox runThroughBlazeTestCheckBox;
   private final JRadioButton[] testingType2RadioButton = new JRadioButton[4];
 
-  BlazeAndroidTestRunConfigurationStateEditor(Project project) {
+  BlazeAndroidTestRunConfigurationStateEditor(
+      RunConfigurationStateEditor commonStateEditor, Project project) {
+    this.commonStateEditor = commonStateEditor;
     setupUI(project);
 
     packageComponent.setComponent(new EditorTextField());
@@ -441,33 +445,37 @@
   }
 
   @Override
-  public void applyEditorTo(BlazeAndroidRunConfigurationState state) {
-    BlazeAndroidTestRunConfigurationState configState =
-        (BlazeAndroidTestRunConfigurationState) state;
-    configState.setRunThroughBlaze(runThroughBlazeTestCheckBox.isSelected());
+  public void applyEditorTo(RunConfigurationState genericState) {
+    BlazeAndroidTestRunConfigurationState state =
+        (BlazeAndroidTestRunConfigurationState) genericState;
+    commonStateEditor.applyEditorTo(state.getCommonState());
 
-    configState.setTestingType(getTestingType());
-    configState.setClassName(classComponent.getComponent().getText());
-    configState.setMethodName(methodComponent.getComponent().getText());
-    configState.setPackageName(packageComponent.getComponent().getText());
-    configState.setInstrumentationRunnerClass(runnerComponent.getComponent().getText());
+    state.setRunThroughBlaze(runThroughBlazeTestCheckBox.isSelected());
+
+    state.setTestingType(getTestingType());
+    state.setClassName(classComponent.getComponent().getText());
+    state.setMethodName(methodComponent.getComponent().getText());
+    state.setPackageName(packageComponent.getComponent().getText());
+    state.setInstrumentationRunnerClass(runnerComponent.getComponent().getText());
   }
 
   @Override
-  public void resetEditorFrom(BlazeAndroidRunConfigurationState state) {
-    BlazeAndroidTestRunConfigurationState configState =
-        (BlazeAndroidTestRunConfigurationState) state;
-    runThroughBlazeTestCheckBox.setSelected(configState.isRunThroughBlaze());
+  public void resetEditorFrom(RunConfigurationState genericState) {
+    BlazeAndroidTestRunConfigurationState state =
+        (BlazeAndroidTestRunConfigurationState) genericState;
+    commonStateEditor.resetEditorFrom(state.getCommonState());
 
-    updateButtonsAndLabelComponents(configState.getTestingType());
-    packageComponent.getComponent().setText(configState.getPackageName());
-    classComponent.getComponent().setText(configState.getClassName());
-    methodComponent.getComponent().setText(configState.getMethodName());
-    runnerComponent.getComponent().setText(configState.getInstrumentationRunnerClass());
+    runThroughBlazeTestCheckBox.setSelected(state.isRunThroughBlaze());
+
+    updateButtonsAndLabelComponents(state.getTestingType());
+    packageComponent.getComponent().setText(state.getPackageName());
+    classComponent.getComponent().setText(state.getClassName());
+    methodComponent.getComponent().setText(state.getMethodName());
+    runnerComponent.getComponent().setText(state.getInstrumentationRunnerClass());
   }
 
   @Override
-  public Component getComponent() {
-    return panel;
+  public JComponent createComponent() {
+    return UiUtil.createBox(commonStateEditor.createComponent(), panel);
   }
 }
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 a12d370..8f2f218 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
@@ -32,6 +32,8 @@
 import com.android.tools.idea.run.util.ProcessHandlerLaunchStatus;
 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.run.deployinfo.BlazeAndroidDeployInfo;
 import com.google.idea.blaze.android.run.deployinfo.BlazeApkProvider;
 import com.google.idea.blaze.android.run.runner.BlazeAndroidDeviceSelector;
 import com.google.idea.blaze.android.run.runner.BlazeAndroidLaunchTasksProvider;
@@ -130,6 +132,18 @@
   @Override
   public ImmutableList<LaunchTask> getDeployTasks(IDevice device, LaunchOptions launchOptions)
       throws ExecutionException {
+    if (!configState.isRunThroughBlaze()) {
+      BlazeAndroidDeployInfo deployInfo =
+          Futures.get(buildStep.getDeployInfo(), ExecutionException.class);
+      if (!deployInfo.getDataToDeploy().isEmpty()) {
+        throw new ExecutionException(
+            "This test target has data dependencies (defined in the 'data' attribute).\n"
+                + "These can only be installed if the configuration is run through 'blaze test'.\n"
+                + "Check the \"Run through 'blaze test'\" checkbox on your "
+                + "run configuration and try again.");
+      }
+    }
+
     Collection<ApkInfo> apks;
     try {
       apks = apkProvider.getApks(device);
@@ -161,10 +175,13 @@
           this,
           launchOptions.isDebug());
     }
+    BlazeAndroidDeployInfo deployInfo =
+        Futures.get(buildStep.getDeployInfo(), ExecutionException.class);
     return StockAndroidTestLaunchTask.getStockTestLaunchTask(
         configState,
         applicationIdProvider,
         launchOptions.isDebug(),
+        deployInfo,
         facet,
         processHandlerLaunchStatus);
   }
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 4ec111f..e17cb97 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
@@ -25,6 +25,7 @@
 import com.android.tools.idea.run.tasks.LaunchTask;
 import com.android.tools.idea.run.testing.AndroidTestListener;
 import com.android.tools.idea.run.util.LaunchStatus;
+import com.google.idea.blaze.android.run.deployinfo.BlazeAndroidDeployInfo;
 import com.intellij.openapi.application.ApplicationManager;
 import com.intellij.openapi.diagnostic.Logger;
 import com.intellij.openapi.util.Computable;
@@ -33,21 +34,20 @@
 import org.jetbrains.android.dom.manifest.Instrumentation;
 import org.jetbrains.android.dom.manifest.Manifest;
 import org.jetbrains.android.facet.AndroidFacet;
-import org.jetbrains.annotations.NotNull;
 import org.jetbrains.annotations.Nullable;
 
 final class StockAndroidTestLaunchTask implements LaunchTask {
   private static final Logger LOG = Logger.getInstance(StockAndroidTestLaunchTask.class);
 
-  @NotNull private final BlazeAndroidTestRunConfigurationState configState;
+  private final BlazeAndroidTestRunConfigurationState configState;
   @Nullable private final String instrumentationTestRunner;
-  @NotNull private final String testApplicationId;
+  private final String testApplicationId;
   private final boolean waitForDebugger;
 
   private StockAndroidTestLaunchTask(
-      @NotNull BlazeAndroidTestRunConfigurationState configState,
+      BlazeAndroidTestRunConfigurationState configState,
       @Nullable String runner,
-      @NotNull String testPackage,
+      String testPackage,
       boolean waitForDebugger) {
     this.configState = configState;
     this.instrumentationTestRunner = runner;
@@ -56,14 +56,15 @@
   }
 
   public static LaunchTask getStockTestLaunchTask(
-      @NotNull BlazeAndroidTestRunConfigurationState configState,
-      @NotNull ApplicationIdProvider applicationIdProvider,
+      BlazeAndroidTestRunConfigurationState configState,
+      ApplicationIdProvider applicationIdProvider,
       boolean waitForDebugger,
-      @NotNull AndroidFacet facet,
-      @NotNull LaunchStatus launchStatus) {
+      BlazeAndroidDeployInfo deployInfo,
+      AndroidFacet facet,
+      LaunchStatus launchStatus) {
     String runner =
         StringUtil.isEmpty(configState.getInstrumentationRunnerClass())
-            ? findInstrumentationRunner(facet)
+            ? findInstrumentationRunner(deployInfo, facet)
             : configState.getInstrumentationRunnerClass();
     String testPackage;
     try {
@@ -81,8 +82,9 @@
   }
 
   @Nullable
-  private static String findInstrumentationRunner(@NotNull AndroidFacet facet) {
-    String runner = getRunnerFromManifest(facet);
+  private static String findInstrumentationRunner(
+      BlazeAndroidDeployInfo deployInfo, AndroidFacet facet) {
+    String runner = getRunnerFromManifest(deployInfo);
 
     // TODO: Resolve direct AndroidGradleModel dep (b/22596984)
     AndroidGradleModel androidModel = AndroidGradleModel.get(facet);
@@ -94,23 +96,22 @@
       }
     }
 
+    // Fall back to the default runner.
+    if (runner == null) {
+      runner = InstrumentationRunnerProvider.getDefaultInstrumentationRunnerClass();
+    }
+
     return runner;
   }
 
   @Nullable
-  private static String getRunnerFromManifest(@NotNull final AndroidFacet facet) {
+  private static String getRunnerFromManifest(final BlazeAndroidDeployInfo deployInfo) {
     if (!ApplicationManager.getApplication().isReadAccessAllowed()) {
       return ApplicationManager.getApplication()
-          .runReadAction(
-              new Computable<String>() {
-                @Override
-                public String compute() {
-                  return getRunnerFromManifest(facet);
-                }
-              });
+          .runReadAction((Computable<String>) () -> getRunnerFromManifest(deployInfo));
     }
 
-    Manifest manifest = facet.getManifest();
+    Manifest manifest = deployInfo.getMergedManifest();
     if (manifest != null) {
       for (Instrumentation instrumentation : manifest.getInstrumentations()) {
         if (instrumentation != null) {
@@ -124,7 +125,6 @@
     return null;
   }
 
-  @NotNull
   @Override
   public String getDescription() {
     return "Launching instrumentation runner";
@@ -137,9 +137,7 @@
 
   @Override
   public boolean perform(
-      @NotNull IDevice device,
-      @NotNull final LaunchStatus launchStatus,
-      @NotNull final ConsolePrinter printer) {
+      IDevice device, final LaunchStatus launchStatus, final ConsolePrinter printer) {
     printer.stdout("Running tests\n");
 
     final RemoteAndroidTestRunner runner =
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 58f99de..162f693 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.sync.workspace.ArtifactLocationDecoder;
 import com.intellij.openapi.project.Project;
 import java.io.File;
 import java.util.Collection;
@@ -36,7 +37,8 @@
     if (syncData.importResult.resourceLibrary == null) {
       return;
     }
-    files.addAll(syncData.importResult.resourceLibrary.sources);
+    ArtifactLocationDecoder artifactLocationDecoder = blazeProjectData.artifactLocationDecoder;
+    files.addAll(artifactLocationDecoder.decodeAll(syncData.importResult.resourceLibrary.sources));
   }
 
   @Override
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 96df410..ffc3edb 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 = androidRuleIdeInfo.idlJar;
     if (idlJar != null) {
-      genJars.add(new BlazeJarLibrary(idlJar, rule.label));
+      genJars.add(new BlazeJarLibrary(idlJar, rule.key));
     }
 
     if (BlazeAndroidWorkspaceImporter.shouldGenerateResources(androidRuleIdeInfo)
@@ -60,7 +60,7 @@
       if (!discardResourceJar) {
         LibraryArtifact resourceJar = androidRuleIdeInfo.resourceJar;
         if (resourceJar != null) {
-          jars.add(new BlazeJarLibrary(resourceJar, rule.label));
+          jars.add(new BlazeJarLibrary(resourceJar, rule.key));
         }
       }
     }
diff --git a/aswb/src/com/google/idea/blaze/android/sync/BlazeAndroidSyncListener.java b/aswb/src/com/google/idea/blaze/android/sync/BlazeAndroidSyncListener.java
index 91d6b57..fda9123 100644
--- a/aswb/src/com/google/idea/blaze/android/sync/BlazeAndroidSyncListener.java
+++ b/aswb/src/com/google/idea/blaze/android/sync/BlazeAndroidSyncListener.java
@@ -16,6 +16,7 @@
 package com.google.idea.blaze.android.sync;
 
 import com.android.tools.idea.res.ResourceFolderRegistry;
+import com.google.idea.blaze.base.scope.BlazeContext;
 import com.google.idea.blaze.base.sync.SyncListener;
 import com.intellij.openapi.project.DumbService;
 import com.intellij.openapi.project.Project;
@@ -23,7 +24,7 @@
 /** Android-specific hooks to run after a blaze sync. */
 public class BlazeAndroidSyncListener extends SyncListener.Adapter {
   @Override
-  public void afterSync(Project project, SyncResult syncResult) {
+  public void afterSync(Project project, BlazeContext context, SyncResult syncResult) {
     if (syncResult == SyncResult.SUCCESS || syncResult == SyncResult.PARTIAL_SUCCESS) {
       DumbService dumbService = DumbService.getInstance(project);
       dumbService.queueTask(new ResourceFolderRegistry.PopulateCachesTask(project));
diff --git a/aswb/src/com/google/idea/blaze/android/sync/BlazeAndroidSyncPlugin.java b/aswb/src/com/google/idea/blaze/android/sync/BlazeAndroidSyncPlugin.java
index 03d8f1b..0755074 100644
--- a/aswb/src/com/google/idea/blaze/android/sync/BlazeAndroidSyncPlugin.java
+++ b/aswb/src/com/google/idea/blaze/android/sync/BlazeAndroidSyncPlugin.java
@@ -26,8 +26,8 @@
 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.base.ideinfo.RuleMap;
 import com.google.idea.blaze.base.model.BlazeProjectData;
-import com.google.idea.blaze.base.model.RuleMap;
 import com.google.idea.blaze.base.model.SyncState;
 import com.google.idea.blaze.base.model.primitives.LanguageClass;
 import com.google.idea.blaze.base.model.primitives.WorkspaceRoot;
@@ -40,6 +40,7 @@
 import com.google.idea.blaze.base.scope.scopes.TimingScope;
 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;
@@ -106,6 +107,7 @@
       BlazeRoots blazeRoots,
       @Nullable WorkingSet workingSet,
       WorkspacePathResolver workspacePathResolver,
+      ArtifactLocationDecoder artifactLocationDecoder,
       RuleMap ruleMap,
       SyncState.Builder syncStateBuilder,
       @Nullable SyncState previousSyncState) {
diff --git a/aswb/src/com/google/idea/blaze/android/sync/importer/BlazeAndroidWorkspaceImporter.java b/aswb/src/com/google/idea/blaze/android/sync/importer/BlazeAndroidWorkspaceImporter.java
index 0439e9b..a519f28 100644
--- a/aswb/src/com/google/idea/blaze/android/sync/importer/BlazeAndroidWorkspaceImporter.java
+++ b/aswb/src/com/google/idea/blaze/android/sync/importer/BlazeAndroidWorkspaceImporter.java
@@ -28,8 +28,8 @@
 import com.google.idea.blaze.base.ideinfo.AndroidRuleIdeInfo;
 import com.google.idea.blaze.base.ideinfo.ArtifactLocation;
 import com.google.idea.blaze.base.ideinfo.RuleIdeInfo;
-import com.google.idea.blaze.base.model.RuleMap;
-import com.google.idea.blaze.base.model.primitives.Label;
+import com.google.idea.blaze.base.ideinfo.RuleKey;
+import com.google.idea.blaze.base.ideinfo.RuleMap;
 import com.google.idea.blaze.base.model.primitives.LanguageClass;
 import com.google.idea.blaze.base.model.primitives.WorkspaceRoot;
 import com.google.idea.blaze.base.projectview.ProjectViewSet;
@@ -37,9 +37,7 @@
 import com.google.idea.blaze.base.scope.output.IssueOutput;
 import com.google.idea.blaze.base.scope.output.PerformanceWarning;
 import com.google.idea.blaze.base.sync.projectview.ProjectViewRuleImportFilter;
-import com.intellij.openapi.diagnostic.Logger;
 import com.intellij.openapi.project.Project;
-import java.io.File;
 import java.util.Collection;
 import java.util.Collections;
 import java.util.List;
@@ -51,11 +49,8 @@
 
 /** Builds a BlazeWorkspace. */
 public final class BlazeAndroidWorkspaceImporter {
-  private static final Logger LOG = Logger.getInstance(BlazeAndroidWorkspaceImporter.class);
 
-  private final Project project;
   private final BlazeContext context;
-  private final WorkspaceRoot workspaceRoot;
   private final RuleMap ruleMap;
   private final ProjectViewRuleImportFilter importFilter;
 
@@ -65,9 +60,7 @@
       WorkspaceRoot workspaceRoot,
       ProjectViewSet projectViewSet,
       RuleMap ruleMap) {
-    this.project = project;
     this.context = context;
-    this.workspaceRoot = workspaceRoot;
     this.ruleMap = ruleMap;
     this.importFilter = new ProjectViewRuleImportFilter(project, workspaceRoot, projectViewSet);
   }
@@ -108,7 +101,7 @@
     assert androidRuleIdeInfo != null;
     if (shouldGenerateResources(androidRuleIdeInfo)
         && shouldGenerateResourceModule(androidRuleIdeInfo)) {
-      AndroidResourceModule.Builder builder = new AndroidResourceModule.Builder(rule.label);
+      AndroidResourceModule.Builder builder = new AndroidResourceModule.Builder(rule.key);
       workspaceBuilder.androidResourceModules.add(builder);
 
       for (ArtifactLocation artifactLocation : androidRuleIdeInfo.resources) {
@@ -120,7 +113,7 @@
       }
 
       TransitiveResourceMap.TransitiveResourceInfo transitiveResourceInfo =
-          transitiveResourceMap.get(rule.label);
+          transitiveResourceMap.get(rule.key);
       for (ArtifactLocation artifactLocation : transitiveResourceInfo.transitiveResources) {
         if (artifactLocation.isSource()) {
           builder.addTransitiveResource(artifactLocation);
@@ -128,8 +121,8 @@
           workspaceBuilder.generatedResourceLocations.add(artifactLocation);
         }
       }
-      for (Label resourceDependency : transitiveResourceInfo.transitiveResourceRules) {
-        if (!resourceDependency.equals(rule.label)) {
+      for (RuleKey resourceDependency : transitiveResourceInfo.transitiveResourceRules) {
+        if (!resourceDependency.equals(rule.key)) {
           builder.addTransitiveResourceDependency(resourceDependency);
         }
       }
@@ -163,7 +156,7 @@
   @Nullable
   private BlazeResourceLibrary createResourceLibrary(
       Collection<AndroidResourceModule> androidResourceModules) {
-    Set<File> result = Sets.newHashSet();
+    Set<ArtifactLocation> result = Sets.newHashSet();
     for (AndroidResourceModule androidResourceModule : androidResourceModules) {
       result.addAll(androidResourceModule.transitiveResources);
     }
@@ -195,7 +188,7 @@
     Multimap<String, AndroidResourceModule> javaPackageToResourceModule =
         ArrayListMultimap.create();
     for (AndroidResourceModule androidResourceModule : androidResourceModules) {
-      RuleIdeInfo rule = ruleMap.get(androidResourceModule.label);
+      RuleIdeInfo rule = ruleMap.get(androidResourceModule.ruleKey);
       AndroidRuleIdeInfo androidRuleIdeInfo = rule.androidRuleIdeInfo;
       assert androidRuleIdeInfo != null;
       javaPackageToResourceModule.put(
@@ -217,7 +210,7 @@
             .append(".R: ");
         messageBuilder.append('\n');
         for (AndroidResourceModule androidResourceModule : androidResourceModulesWithJavaPackage) {
-          messageBuilder.append("  ").append(androidResourceModule.label).append('\n');
+          messageBuilder.append("  ").append(androidResourceModule.ruleKey).append('\n');
         }
         String message = messageBuilder.toString();
         context.output(new PerformanceWarning(message));
@@ -227,7 +220,7 @@
       }
     }
 
-    Collections.sort(result, (lhs, rhs) -> Label.COMPARATOR.compare(lhs.label, rhs.label));
+    Collections.sort(result, (lhs, rhs) -> RuleKey.COMPARATOR.compare(lhs.ruleKey, rhs.ruleKey));
     return ImmutableList.copyOf(result);
   }
 
@@ -243,8 +236,8 @@
                         lhs.transitiveResources.size(),
                         rhs.transitiveResources.size()) // Most transitive resources wins
                     .compare(
-                        rhs.label.toString().length(),
-                        lhs.label
+                        rhs.ruleKey.toString().length(),
+                        lhs.ruleKey
                             .toString()
                             .length()) // Shortest label wins - note lhs, rhs are flipped
                     .result())
diff --git a/aswb/src/com/google/idea/blaze/android/sync/importer/aggregators/RuleIdeInfoTransitiveAggregator.java b/aswb/src/com/google/idea/blaze/android/sync/importer/aggregators/RuleIdeInfoTransitiveAggregator.java
index 3a7b95c..39a7ab4 100644
--- a/aswb/src/com/google/idea/blaze/android/sync/importer/aggregators/RuleIdeInfoTransitiveAggregator.java
+++ b/aswb/src/com/google/idea/blaze/android/sync/importer/aggregators/RuleIdeInfoTransitiveAggregator.java
@@ -16,7 +16,7 @@
 package com.google.idea.blaze.android.sync.importer.aggregators;
 
 import com.google.idea.blaze.base.ideinfo.RuleIdeInfo;
-import com.google.idea.blaze.base.model.RuleMap;
+import com.google.idea.blaze.base.ideinfo.RuleMap;
 import com.google.idea.blaze.base.model.primitives.Label;
 
 /** Transitive aggregator for RuleIdeInfo. */
diff --git a/aswb/src/com/google/idea/blaze/android/sync/importer/aggregators/TransitiveAggregator.java b/aswb/src/com/google/idea/blaze/android/sync/importer/aggregators/TransitiveAggregator.java
index 5048910..d2145f3 100644
--- a/aswb/src/com/google/idea/blaze/android/sync/importer/aggregators/TransitiveAggregator.java
+++ b/aswb/src/com/google/idea/blaze/android/sync/importer/aggregators/TransitiveAggregator.java
@@ -17,36 +17,36 @@
 
 import com.google.common.collect.Maps;
 import com.google.idea.blaze.base.ideinfo.RuleIdeInfo;
-import com.google.idea.blaze.base.model.RuleMap;
+import com.google.idea.blaze.base.ideinfo.RuleKey;
+import com.google.idea.blaze.base.ideinfo.RuleMap;
 import com.google.idea.blaze.base.model.primitives.Label;
 import java.util.Map;
 import org.jetbrains.annotations.Nullable;
 
 /** Peforms a transitive reduction on the rule */
 public abstract class TransitiveAggregator<T> {
-  private Map<Label, T> labelToResult;
+  private Map<RuleKey, T> ruleKeyToResult;
 
   protected TransitiveAggregator(RuleMap ruleMap) {
-    this.labelToResult = Maps.newHashMap();
+    this.ruleKeyToResult = Maps.newHashMap();
     for (RuleIdeInfo rule : ruleMap.rules()) {
-      Label label = rule.label;
-      aggregate(label, ruleMap);
+      aggregate(rule.key, ruleMap);
     }
   }
 
-  protected T getOrDefault(Label key, T defaultValue) {
-    T result = labelToResult.get(key);
+  protected T getOrDefault(RuleKey ruleKey, T defaultValue) {
+    T result = ruleKeyToResult.get(ruleKey);
     return result != null ? result : defaultValue;
   }
 
   @Nullable
-  private T aggregate(Label label, RuleMap ruleMap) {
-    T result = labelToResult.get(label);
+  private T aggregate(RuleKey ruleKey, RuleMap ruleMap) {
+    T result = ruleKeyToResult.get(ruleKey);
     if (result != null) {
       return result;
     }
 
-    RuleIdeInfo rule = ruleMap.get(label);
+    RuleIdeInfo rule = ruleMap.get(ruleKey);
     if (rule == null) {
       return null;
     }
@@ -54,13 +54,13 @@
     result = createForRule(rule);
 
     for (Label depLabel : getDependencies(rule)) {
-      T depResult = aggregate(depLabel, ruleMap);
+      T depResult = aggregate(RuleKey.forDependency(rule, depLabel), ruleMap);
       if (depResult != null) {
         result = reduce(result, depResult);
       }
     }
 
-    labelToResult.put(label, result);
+    ruleKeyToResult.put(ruleKey, result);
     return result;
   }
 
diff --git a/aswb/src/com/google/idea/blaze/android/sync/importer/aggregators/TransitiveResourceMap.java b/aswb/src/com/google/idea/blaze/android/sync/importer/aggregators/TransitiveResourceMap.java
index f49029d..366e9fd 100644
--- a/aswb/src/com/google/idea/blaze/android/sync/importer/aggregators/TransitiveResourceMap.java
+++ b/aswb/src/com/google/idea/blaze/android/sync/importer/aggregators/TransitiveResourceMap.java
@@ -20,7 +20,8 @@
 import com.google.idea.blaze.base.ideinfo.AndroidRuleIdeInfo;
 import com.google.idea.blaze.base.ideinfo.ArtifactLocation;
 import com.google.idea.blaze.base.ideinfo.RuleIdeInfo;
-import com.google.idea.blaze.base.model.RuleMap;
+import com.google.idea.blaze.base.ideinfo.RuleKey;
+import com.google.idea.blaze.base.ideinfo.RuleMap;
 import com.google.idea.blaze.base.model.primitives.Label;
 import java.util.List;
 import java.util.Set;
@@ -32,7 +33,7 @@
   public static class TransitiveResourceInfo {
     public static final TransitiveResourceInfo NO_RESOURCES = new TransitiveResourceInfo();
     public final Set<ArtifactLocation> transitiveResources = Sets.newHashSet();
-    public final Set<Label> transitiveResourceRules = Sets.newHashSet();
+    public final Set<RuleKey> transitiveResourceRules = Sets.newHashSet();
   }
 
   public TransitiveResourceMap(RuleMap ruleMap) {
@@ -50,8 +51,8 @@
     return super.getDependencies(ruleIdeInfo);
   }
 
-  public TransitiveResourceInfo get(Label label) {
-    return getOrDefault(label, TransitiveResourceInfo.NO_RESOURCES);
+  public TransitiveResourceInfo get(RuleKey ruleKey) {
+    return getOrDefault(ruleKey, TransitiveResourceInfo.NO_RESOURCES);
   }
 
   @Override
@@ -65,7 +66,7 @@
       return result;
     }
     result.transitiveResources.addAll(androidRuleIdeInfo.resources);
-    result.transitiveResourceRules.add(ruleIdeInfo.label);
+    result.transitiveResourceRules.add(ruleIdeInfo.key);
     return result;
   }
 
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 e4807a9..d8f23d6 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
@@ -20,8 +20,8 @@
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.Sets;
 import com.google.idea.blaze.base.ideinfo.ArtifactLocation;
+import com.google.idea.blaze.base.ideinfo.RuleKey;
 import com.google.idea.blaze.base.model.primitives.Label;
-import java.io.File;
 import java.io.Serializable;
 import java.util.List;
 import java.util.Set;
@@ -35,19 +35,19 @@
  */
 @Immutable
 public final class AndroidResourceModule implements Serializable {
-  private static final long serialVersionUID = 5L;
+  private static final long serialVersionUID = 8L;
 
-  public final Label label;
-  public final ImmutableCollection<File> resources;
-  public final ImmutableCollection<File> transitiveResources;
-  public final ImmutableCollection<Label> transitiveResourceDependencies;
+  public final RuleKey ruleKey;
+  public final ImmutableCollection<ArtifactLocation> resources;
+  public final ImmutableCollection<ArtifactLocation> transitiveResources;
+  public final ImmutableCollection<RuleKey> transitiveResourceDependencies;
 
   public AndroidResourceModule(
-      Label label,
-      ImmutableCollection<File> resources,
-      ImmutableCollection<File> transitiveResources,
-      ImmutableCollection<Label> transitiveResourceDependencies) {
-    this.label = label;
+      RuleKey ruleKey,
+      ImmutableCollection<ArtifactLocation> resources,
+      ImmutableCollection<ArtifactLocation> transitiveResources,
+      ImmutableCollection<RuleKey> transitiveResourceDependencies) {
+    this.ruleKey = ruleKey;
     this.resources = resources;
     this.transitiveResources = transitiveResources;
     this.transitiveResourceDependencies = transitiveResourceDependencies;
@@ -57,7 +57,7 @@
   public boolean equals(Object o) {
     if (o instanceof AndroidResourceModule) {
       AndroidResourceModule that = (AndroidResourceModule) o;
-      return Objects.equal(this.label, that.label)
+      return Objects.equal(this.ruleKey, that.ruleKey)
           && Objects.equal(this.resources, that.resources)
           && Objects.equal(this.transitiveResources, that.transitiveResources)
           && Objects.equal(
@@ -69,15 +69,18 @@
   @Override
   public int hashCode() {
     return Objects.hashCode(
-        this.label, this.resources, this.transitiveResources, this.transitiveResourceDependencies);
+        this.ruleKey,
+        this.resources,
+        this.transitiveResources,
+        this.transitiveResourceDependencies);
   }
 
   @Override
   public String toString() {
     return "AndroidResourceModule{"
         + "\n"
-        + "  label: "
-        + label
+        + "  rule: "
+        + ruleKey
         + "\n"
         + "  resources: "
         + resources
@@ -91,8 +94,8 @@
         + '}';
   }
 
-  public static Builder builder(Label label) {
-    return new Builder(label);
+  public static Builder builder(RuleKey ruleKey) {
+    return new Builder(ruleKey);
   }
 
   public boolean isEmpty() {
@@ -101,13 +104,13 @@
 
   /** Builder for the resource module */
   public static class Builder {
-    private final Label label;
+    private final RuleKey ruleKey;
     private final Set<ArtifactLocation> resources = Sets.newHashSet();
     private final Set<ArtifactLocation> transitiveResources = Sets.newHashSet();
-    private Set<Label> transitiveResourceDependencies = Sets.newHashSet();
+    private Set<RuleKey> transitiveResourceDependencies = Sets.newHashSet();
 
-    public Builder(Label label) {
-      this.label = label;
+    public Builder(RuleKey ruleKey) {
+      this.ruleKey = ruleKey;
     }
 
     public Builder addResource(ArtifactLocation resource) {
@@ -131,11 +134,16 @@
       return this;
     }
 
-    public Builder addTransitiveResourceDependency(Label dependency) {
+    public Builder addTransitiveResourceDependency(RuleKey dependency) {
       this.transitiveResourceDependencies.add(dependency);
       return this;
     }
 
+    public Builder addTransitiveResourceDependency(Label dependency) {
+      this.transitiveResourceDependencies.add(RuleKey.forPlainTarget(dependency));
+      return this;
+    }
+
     public Builder addTransitiveResourceDependency(String dependency) {
       return addTransitiveResourceDependency(new Label(dependency));
     }
@@ -143,17 +151,15 @@
     @NotNull
     public AndroidResourceModule build() {
       return new AndroidResourceModule(
-          label,
+          ruleKey,
           ImmutableList.copyOf(
               resources
                   .stream()
-                  .map(ArtifactLocation::getFile)
                   .sorted()
                   .collect(Collectors.toList())),
           ImmutableList.copyOf(
               transitiveResources
                   .stream()
-                  .map(ArtifactLocation::getFile)
                   .sorted()
                   .collect(Collectors.toList())),
           ImmutableList.copyOf(
diff --git a/aswb/src/com/google/idea/blaze/android/sync/model/BlazeResourceLibrary.java b/aswb/src/com/google/idea/blaze/android/sync/model/BlazeResourceLibrary.java
index 240424e..9489a63 100644
--- a/aswb/src/com/google/idea/blaze/android/sync/model/BlazeResourceLibrary.java
+++ b/aswb/src/com/google/idea/blaze/android/sync/model/BlazeResourceLibrary.java
@@ -17,22 +17,23 @@
 
 import com.google.common.base.Objects;
 import com.google.common.collect.ImmutableList;
+import com.google.idea.blaze.base.ideinfo.ArtifactLocation;
+import com.google.idea.blaze.base.sync.workspace.ArtifactLocationDecoder;
 import com.google.idea.blaze.java.sync.model.BlazeLibrary;
 import com.google.idea.blaze.java.sync.model.LibraryKey;
 import com.intellij.openapi.project.Project;
 import com.intellij.openapi.roots.OrderRootType;
 import com.intellij.openapi.roots.libraries.Library;
-import java.io.File;
 import javax.annotation.concurrent.Immutable;
 
 /** A library that contains sources. */
 @Immutable
 public final class BlazeResourceLibrary extends BlazeLibrary {
-  private static final long serialVersionUID = 1L;
+  private static final long serialVersionUID = 2L;
 
-  public final ImmutableList<File> sources;
+  public final ImmutableList<ArtifactLocation> sources;
 
-  public BlazeResourceLibrary(ImmutableList<File> sources) {
+  public BlazeResourceLibrary(ImmutableList<ArtifactLocation> sources) {
     super(LibraryKey.forResourceLibrary());
     this.sources = sources;
   }
@@ -57,9 +58,12 @@
   }
 
   @Override
-  public void modifyLibraryModel(Project project, Library.ModifiableModel libraryModel) {
-    for (File file : sources) {
-      libraryModel.addRoot(pathToUrl(file), OrderRootType.SOURCES);
+  public void modifyLibraryModel(
+      Project project,
+      ArtifactLocationDecoder artifactLocationDecoder,
+      Library.ModifiableModel libraryModel) {
+    for (ArtifactLocation file : sources) {
+      libraryModel.addRoot(pathToUrl(artifactLocationDecoder.decode(file)), OrderRootType.SOURCES);
     }
   }
 }
diff --git a/aswb/src/com/google/idea/blaze/android/sync/model/idea/BlazeClassJarProvider.java b/aswb/src/com/google/idea/blaze/android/sync/model/idea/BlazeClassJarProvider.java
index f6372ed..dbd24d6 100644
--- a/aswb/src/com/google/idea/blaze/android/sync/model/idea/BlazeClassJarProvider.java
+++ b/aswb/src/com/google/idea/blaze/android/sync/model/idea/BlazeClassJarProvider.java
@@ -21,6 +21,7 @@
 import com.google.idea.blaze.base.ideinfo.LibraryArtifact;
 import com.google.idea.blaze.base.model.BlazeProjectData;
 import com.google.idea.blaze.base.sync.data.BlazeProjectDataManager;
+import com.google.idea.blaze.base.sync.workspace.ArtifactLocationDecoder;
 import com.google.idea.blaze.java.sync.model.BlazeJarLibrary;
 import com.google.idea.blaze.java.sync.model.BlazeJavaSyncData;
 import com.intellij.openapi.module.Module;
@@ -56,7 +57,8 @@
     if (syncData == null) {
       return null;
     }
-    for (File classJar : syncData.importResult.buildOutputJars) {
+    ArtifactLocationDecoder artifactLocationDecoder = blazeProjectData.artifactLocationDecoder;
+    for (File classJar : artifactLocationDecoder.decodeAll(syncData.importResult.buildOutputJars)) {
       VirtualFile classJarVF = localVfs.findFileByIoFile(classJar);
       if (classJarVF == null) {
         continue;
@@ -90,6 +92,7 @@
     if (syncData == null) {
       return null;
     }
+    ArtifactLocationDecoder artifactLocationDecoder = blazeProjectData.artifactLocationDecoder;
     LocalFileSystem localVfs = LocalFileSystem.getInstance();
     for (BlazeJarLibrary blazeLibrary : syncData.importResult.libraries.values()) {
       LibraryArtifact libraryArtifact = blazeLibrary.libraryArtifact;
@@ -97,7 +100,7 @@
       if (classJar == null) {
         continue;
       }
-      VirtualFile libVF = localVfs.findFileByIoFile(classJar.getFile());
+      VirtualFile libVF = localVfs.findFileByIoFile(artifactLocationDecoder.decode(classJar));
       if (libVF == null) {
         continue;
       }
diff --git a/aswb/src/com/google/idea/blaze/android/sync/model/idea/SourceProviderImpl.java b/aswb/src/com/google/idea/blaze/android/sync/model/idea/SourceProviderImpl.java
index 2bf8e82..06a8444 100644
--- a/aswb/src/com/google/idea/blaze/android/sync/model/idea/SourceProviderImpl.java
+++ b/aswb/src/com/google/idea/blaze/android/sync/model/idea/SourceProviderImpl.java
@@ -37,7 +37,7 @@
   public SourceProviderImpl(String name, File manifestFile, Collection<File> resDirs) {
     this.name = name;
     this.manifestFile = manifestFile;
-    this.resDirs = resDirs;
+    this.resDirs = ImmutableList.copyOf(resDirs);
   }
 
   @Override
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 5fc8793..aff9a7e 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
@@ -16,7 +16,6 @@
 package com.google.idea.blaze.android.sync.projectstructure;
 
 import com.android.builder.model.SourceProvider;
-import com.google.common.collect.ImmutableCollection;
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.Maps;
 import com.google.common.collect.Sets;
@@ -31,6 +30,7 @@
 import com.google.idea.blaze.base.ideinfo.AndroidRuleIdeInfo;
 import com.google.idea.blaze.base.ideinfo.ArtifactLocation;
 import com.google.idea.blaze.base.ideinfo.RuleIdeInfo;
+import com.google.idea.blaze.base.ideinfo.RuleKey;
 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;
@@ -42,6 +42,7 @@
 import com.google.idea.blaze.base.scope.output.PrintOutput;
 import com.google.idea.blaze.base.sync.BlazeSyncPlugin;
 import com.google.idea.blaze.base.sync.data.BlazeProjectDataManager;
+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;
@@ -51,6 +52,7 @@
 import com.intellij.openapi.project.Project;
 import com.intellij.openapi.roots.ModifiableRootModel;
 import java.io.File;
+import java.util.Collection;
 import java.util.Map;
 import java.util.Set;
 import javax.annotation.Nullable;
@@ -87,21 +89,21 @@
 
         // Create android resource modules
         // Because we're setting up dependencies, the modules have to exist before we configure them
-        Map<Label, AndroidResourceModule> labelToAndroidResourceModule = Maps.newHashMap();
+        Map<RuleKey, AndroidResourceModule> ruleToAndroidResourceModule = Maps.newHashMap();
         for (AndroidResourceModule androidResourceModule :
             syncData.importResult.androidResourceModules) {
-          labelToAndroidResourceModule.put(androidResourceModule.label, androidResourceModule);
-          String moduleName = moduleNameForAndroidModule(androidResourceModule.label);
+          ruleToAndroidResourceModule.put(androidResourceModule.ruleKey, androidResourceModule);
+          String moduleName = moduleNameForAndroidModule(androidResourceModule.ruleKey);
           moduleEditor.createModule(moduleName, StdModuleTypes.JAVA);
         }
 
         // Configure android resource modules
-        for (AndroidResourceModule androidResourceModule : labelToAndroidResourceModule.values()) {
-          RuleIdeInfo rule = blazeProjectData.ruleMap.get(androidResourceModule.label);
+        for (AndroidResourceModule androidResourceModule : ruleToAndroidResourceModule.values()) {
+          RuleIdeInfo rule = blazeProjectData.ruleMap.get(androidResourceModule.ruleKey);
           AndroidRuleIdeInfo androidRuleIdeInfo = rule.androidRuleIdeInfo;
           assert androidRuleIdeInfo != null;
 
-          String moduleName = moduleNameForAndroidModule(rule.label);
+          String moduleName = moduleNameForAndroidModule(rule.key);
           Module module = moduleEditor.findModule(moduleName);
           assert module != null;
           ModifiableRootModel modifiableRootModel = moduleEditor.editModule(module);
@@ -109,14 +111,15 @@
           updateAndroidRuleModule(
               project,
               workspaceRoot,
+              blazeProjectData.artifactLocationDecoder,
               androidSdkPlatform,
               rule,
               module,
               modifiableRootModel,
               androidResourceModule);
 
-          for (Label resourceDependency : androidResourceModule.transitiveResourceDependencies) {
-            if (!labelToAndroidResourceModule.containsKey(resourceDependency)) {
+          for (RuleKey resourceDependency : androidResourceModule.transitiveResourceDependencies) {
+            if (!ruleToAndroidResourceModule.containsKey(resourceDependency)) {
               continue;
             }
             String dependencyModuleName = moduleNameForAndroidModule(resourceDependency);
@@ -157,12 +160,13 @@
 
         int totalRunConfigurationModules = 0;
         for (Label label : runConfigurationModuleTargets) {
+          RuleKey ruleKey = RuleKey.forPlainTarget(label);
           // If it's a resource module, it will already have been created
-          if (labelToAndroidResourceModule.containsKey(label)) {
+          if (ruleToAndroidResourceModule.containsKey(ruleKey)) {
             continue;
           }
           // Ensure the label is a supported android rule that exists
-          RuleIdeInfo rule = blazeProjectData.ruleMap.get(label);
+          RuleIdeInfo rule = blazeProjectData.ruleMap.get(ruleKey);
           if (rule == null) {
             continue;
           }
@@ -170,11 +174,18 @@
             continue;
           }
 
-          String moduleName = moduleNameForAndroidModule(rule.label);
+          String moduleName = moduleNameForAndroidModule(ruleKey);
           Module module = moduleEditor.createModule(moduleName, StdModuleTypes.JAVA);
           ModifiableRootModel modifiableRootModel = moduleEditor.editModule(module);
           updateAndroidRuleModule(
-              project, workspaceRoot, androidSdkPlatform, rule, module, modifiableRootModel, null);
+              project,
+              workspaceRoot,
+              blazeProjectData.artifactLocationDecoder,
+              androidSdkPlatform,
+              rule,
+              module,
+              modifiableRootModel,
+              null);
           ++totalRunConfigurationModules;
         }
 
@@ -196,7 +207,8 @@
   /** Ensures a suitable module exists for the given android target. */
   @Nullable
   public static Module ensureRunConfigurationModule(Project project, Label target) {
-    String moduleName = moduleNameForAndroidModule(target);
+    RuleKey ruleKey = RuleKey.forPlainTarget(target);
+    String moduleName = moduleNameForAndroidModule(ruleKey);
     Module module = ModuleManager.getInstance(project).findModuleByName(moduleName);
     if (module != null) {
       return module;
@@ -213,7 +225,7 @@
     if (androidSdkPlatform == null) {
       return null;
     }
-    RuleIdeInfo rule = blazeProjectData.ruleMap.get(target);
+    RuleIdeInfo rule = blazeProjectData.ruleMap.get(ruleKey);
     if (rule == null) {
       return null;
     }
@@ -237,6 +249,7 @@
               updateAndroidRuleModule(
                   project,
                   workspaceRoot,
+                  blazeProjectData.artifactLocationDecoder,
                   androidSdkPlatform,
                   rule,
                   newModule,
@@ -247,8 +260,8 @@
     return newModule;
   }
 
-  public static String moduleNameForAndroidModule(Label label) {
-    return label
+  public static String moduleNameForAndroidModule(RuleKey ruleKey) {
+    return ruleKey
         .toString()
         .substring(2) // Skip initial "//"
         .replace('/', '.')
@@ -280,17 +293,20 @@
   private static void updateAndroidRuleModule(
       Project project,
       WorkspaceRoot workspaceRoot,
+      ArtifactLocationDecoder artifactLocationDecoder,
       AndroidSdkPlatform androidSdkPlatform,
       RuleIdeInfo rule,
       Module module,
       ModifiableRootModel modifiableRootModel,
       @Nullable AndroidResourceModule androidResourceModule) {
 
-    ImmutableCollection<File> resources =
-        androidResourceModule != null ? androidResourceModule.resources : ImmutableList.of();
-    ImmutableCollection<File> transitiveResources =
+    Collection<File> resources =
         androidResourceModule != null
-            ? androidResourceModule.transitiveResources
+            ? artifactLocationDecoder.decodeAll(androidResourceModule.resources)
+            : ImmutableList.of();
+    Collection<File> transitiveResources =
+        androidResourceModule != null
+            ? artifactLocationDecoder.decodeAll(androidResourceModule.transitiveResources)
             : ImmutableList.of();
 
     AndroidRuleIdeInfo androidRuleIdeInfo = rule.androidRuleIdeInfo;
@@ -300,7 +316,7 @@
     ArtifactLocation manifestArtifactLocation = androidRuleIdeInfo.manifest;
     File manifest =
         manifestArtifactLocation != null
-            ? manifestArtifactLocation.getFile()
+            ? artifactLocationDecoder.decode(manifestArtifactLocation)
             : new File(moduleDirectory, "AndroidManifest.xml");
     String resourceJavaPackage = androidRuleIdeInfo.resourceJavaPackage;
     ResourceModuleContentRootCustomizer.setupContentRoots(modifiableRootModel, resources);
@@ -322,7 +338,7 @@
       File moduleDirectory,
       File manifest,
       String resourceJavaPackage,
-      ImmutableCollection<File> transitiveResources) {
+      Collection<File> transitiveResources) {
     AndroidFacetModuleCustomizer.createAndroidFacet(module);
     SourceProvider sourceProvider =
         new SourceProviderImpl(module.getName(), manifest, transitiveResources);
diff --git a/aswb/tests/unittests/com/google/idea/blaze/android/run/BlazeAndroidRunConfigurationCommonStateTest.java b/aswb/tests/unittests/com/google/idea/blaze/android/run/BlazeAndroidRunConfigurationCommonStateTest.java
deleted file mode 100644
index 761643d..0000000
--- a/aswb/tests/unittests/com/google/idea/blaze/android/run/BlazeAndroidRunConfigurationCommonStateTest.java
+++ /dev/null
@@ -1,93 +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.common.collect.ImmutableList;
-import com.google.common.collect.Lists;
-import com.google.idea.blaze.android.cppapi.NdkSupport;
-import com.google.idea.blaze.base.BlazeTestCase;
-import com.google.idea.common.experiments.ExperimentService;
-import com.google.idea.common.experiments.MockExperimentService;
-import com.intellij.openapi.util.InvalidDataException;
-import com.intellij.openapi.util.WriteExternalException;
-import org.jdom.Element;
-import org.jetbrains.annotations.NotNull;
-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 BlazeTestCase {
-  private BlazeAndroidRunConfigurationCommonState commonState;
-
-  @Override
-  protected void initTest(
-      @NotNull Container applicationServices, @NotNull Container projectServices) {
-    super.initTest(applicationServices, projectServices);
-
-    MockExperimentService experimentService = new MockExperimentService();
-    applicationServices.register(ExperimentService.class, experimentService);
-    // BlazeAndroidRunConfigurationCommonState.isNativeDebuggingEnabled() always
-    // returns false if this experiment is false.
-    experimentService.setExperiment(NdkSupport.NDK_SUPPORT, true);
-
-    commonState = new BlazeAndroidRunConfigurationCommonState(ImmutableList.of());
-  }
-
-  @Test
-  public void readAndWriteShouldMatch() throws InvalidDataException, WriteExternalException {
-    commonState.setUserFlags(ImmutableList.of("--flag1", "--flag2"));
-    commonState.setNativeDebuggingEnabled(true);
-
-    Element element = new Element("test");
-    commonState.writeExternal(element);
-    BlazeAndroidRunConfigurationCommonState readCommonState =
-        new BlazeAndroidRunConfigurationCommonState(ImmutableList.of());
-    readCommonState.readExternal(element);
-
-    assertThat(readCommonState.getUserFlags()).containsExactly("--flag1", "--flag2").inOrder();
-    assertThat(readCommonState.isNativeDebuggingEnabled()).isTrue();
-  }
-
-  @Test
-  public void readAndWriteShouldHandleNulls() throws InvalidDataException, WriteExternalException {
-    Element element = new Element("test");
-    commonState.writeExternal(element);
-    BlazeAndroidRunConfigurationCommonState readCommonState =
-        new BlazeAndroidRunConfigurationCommonState(ImmutableList.of());
-    readCommonState.readExternal(element);
-
-    assertThat(readCommonState.getUserFlags()).isEqualTo(commonState.getUserFlags());
-    assertThat(readCommonState.isNativeDebuggingEnabled())
-        .isEqualTo(commonState.isNativeDebuggingEnabled());
-  }
-
-  @Test
-  public void readShouldOmitEmptyFlags() throws InvalidDataException, WriteExternalException {
-    commonState.setUserFlags(Lists.newArrayList("hi ", "", "I'm", " ", "\t", "Josh\r\n", "\n"));
-
-    Element element = new Element("test");
-    commonState.writeExternal(element);
-    BlazeAndroidRunConfigurationCommonState readCommonState =
-        new BlazeAndroidRunConfigurationCommonState(ImmutableList.of());
-    readCommonState.readExternal(element);
-
-    assertThat(readCommonState.getUserFlags()).containsExactly("hi", "I'm", "Josh").inOrder();
-  }
-}
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 dddf2f5..985db7b 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
@@ -30,8 +30,9 @@
 import com.google.idea.blaze.base.ideinfo.JavaRuleIdeInfo;
 import com.google.idea.blaze.base.ideinfo.LibraryArtifact;
 import com.google.idea.blaze.base.ideinfo.RuleIdeInfo;
+import com.google.idea.blaze.base.ideinfo.RuleKey;
+import com.google.idea.blaze.base.ideinfo.RuleMap;
 import com.google.idea.blaze.base.ideinfo.RuleMapBuilder;
-import com.google.idea.blaze.base.model.RuleMap;
 import com.google.idea.blaze.base.model.primitives.Label;
 import com.google.idea.blaze.base.model.primitives.WorkspacePath;
 import com.google.idea.blaze.base.model.primitives.WorkspaceRoot;
@@ -62,16 +63,11 @@
 @RunWith(JUnit4.class)
 public class BlazeAndroidWorkspaceImporterTest extends BlazeTestCase {
 
-  private static final String FAKE_ROOT = "/root";
-  private WorkspaceRoot workspaceRoot = new WorkspaceRoot(new File(FAKE_ROOT));
+  private final WorkspaceRoot workspaceRoot = new WorkspaceRoot(new File("/root"));
 
   private static final String FAKE_GEN_ROOT_EXECUTION_PATH_FRAGMENT =
       "blaze-out/gcc-4.X.Y-crosstool-v17-hybrid-grtev3-k8-fastbuild/bin";
 
-  private static final String FAKE_GEN_ROOT =
-      "/abs_root/_blaze_user/8093958afcfde6c33d08b621dfaa4e09/root/"
-          + FAKE_GEN_ROOT_EXECUTION_PATH_FRAGMENT;
-
   private static final BlazeImportSettings DUMMY_IMPORT_SETTINGS =
       new BlazeImportSettings("", "", "", "", "", BuildSystem.Blaze);
 
@@ -204,7 +200,8 @@
 
     assertThat(result.androidResourceModules)
         .containsExactly(
-            AndroidResourceModule.builder(new Label("//java/apps/example:example_debug"))
+            AndroidResourceModule.builder(
+                    RuleKey.forPlainTarget(new Label("//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"))
@@ -213,14 +210,16 @@
                 .addTransitiveResourceDependency("//java/apps/example/lib1:lib1")
                 .addTransitiveResourceDependency("//java/libraries/shared:shared")
                 .build(),
-            AndroidResourceModule.builder(new Label("//java/apps/example/lib0:lib0"))
+            AndroidResourceModule.builder(
+                    RuleKey.forPlainTarget(new Label("//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"))
                 .addTransitiveResourceDependency("//java/apps/example/lib1:lib1")
                 .addTransitiveResourceDependency("//java/libraries/shared:shared")
                 .build(),
-            AndroidResourceModule.builder(new Label("//java/apps/example/lib1:lib1"))
+            AndroidResourceModule.builder(
+                    RuleKey.forPlainTarget(new Label("//java/apps/example/lib1:lib1")))
                 .addResourceAndTransitiveResource(source("java/apps/example/lib1/res"))
                 .addTransitiveResource(source("java/libraries/shared/res"))
                 .addTransitiveResourceDependency("//java/libraries/shared:shared")
@@ -366,7 +365,8 @@
 
     assertThat(
             jars.stream()
-                .map(library -> library.libraryArtifact.interfaceJar.getFile().getName())
+                .map(library -> library.libraryArtifact.interfaceJar)
+                .map(artifactLocation -> new File(artifactLocation.relativePath).getName())
                 .collect(Collectors.toList()))
         .containsExactly("lib0_resources.jar");
   }
@@ -414,7 +414,8 @@
     assertThat(
             genJars
                 .stream()
-                .map(library -> library.libraryArtifact.interfaceJar.getFile().getName())
+                .map(library -> library.libraryArtifact.interfaceJar)
+                .map(artifactLocation -> new File(artifactLocation.relativePath).getName())
                 .collect(Collectors.toList()))
         .containsExactly("libidl.jar");
   }
@@ -460,7 +461,8 @@
     errorCollector.assertNoIssues();
     assertThat(result.androidResourceModules)
         .containsExactly(
-            AndroidResourceModule.builder(new Label("//java/example:resources"))
+            AndroidResourceModule.builder(
+                    RuleKey.forPlainTarget(new Label("//java/example:resources")))
                 .addResourceAndTransitiveResource(source("java/example/res"))
                 .build());
   }
@@ -506,7 +508,12 @@
     errorCollector.assertNoIssues();
     BlazeResourceLibrary library = result.resourceLibrary;
     assertThat(library).isNotNull();
-    assertThat(library.sources).containsExactly(new File("/root/java/example2/res"));
+    assertThat(library.sources)
+        .containsExactly(
+            ArtifactLocation.builder()
+                .setRelativePath("java/example2/res")
+                .setIsSource(true)
+                .build());
   }
 
   @Test
@@ -551,7 +558,7 @@
 
     assertThat(result.androidResourceModules)
         .containsExactly(
-            AndroidResourceModule.builder(new Label("//java/example:lib"))
+            AndroidResourceModule.builder(RuleKey.forPlainTarget(new Label("//java/example:lib")))
                 .addResourceAndTransitiveResource(source("java/example/res"))
                 .build());
   }
@@ -586,16 +593,11 @@
   }
 
   private ArtifactLocation source(String relativePath) {
-    return ArtifactLocation.builder()
-        .setRootPath(FAKE_ROOT)
-        .setRelativePath(relativePath)
-        .setIsSource(true)
-        .build();
+    return ArtifactLocation.builder().setRelativePath(relativePath).setIsSource(true).build();
   }
 
   private static ArtifactLocation gen(String relativePath) {
     return ArtifactLocation.builder()
-        .setRootPath(FAKE_GEN_ROOT)
         .setRootExecutionPathFragment(FAKE_GEN_ROOT_EXECUTION_PATH_FRAGMENT)
         .setRelativePath(relativePath)
         .setIsSource(false)
diff --git a/base/BUILD b/base/BUILD
index add4fdb..0cd244d 100644
--- a/base/BUILD
+++ b/base/BUILD
@@ -42,7 +42,9 @@
         "//base",
         "//intellij_platform_sdk:plugin_api_for_tests",
         "//proto_deps",
+        "//testing:lib",
         "@jsr305_annotations//jar",
+        "@junit//jar",
     ],
 )
 
@@ -69,7 +71,7 @@
 )
 
 load(
-    "//intellij_test:test_defs.bzl",
+    "//testing:test_defs.bzl",
     "intellij_integration_test_suite",
     "intellij_unit_test_suite",
 )
diff --git a/base/scripts/create_bugreport.sh b/base/scripts/create_bugreport.sh
index 8923bae..d0c40d7 100755
--- a/base/scripts/create_bugreport.sh
+++ b/base/scripts/create_bugreport.sh
@@ -46,7 +46,7 @@
 tar_dir=$tmp_dir/$output_name
 output_file=${output_name}.tar.gz
 
-mkdir -p tar_dir
+mkdir -p "$tar_dir"
 [ $? -eq 0 ] || { exit 1; }
 
 # Attach process information
diff --git a/base/src/META-INF/blaze-base.xml b/base/src/META-INF/blaze-base.xml
index 606ea98..7559da7 100644
--- a/base/src/META-INF/blaze-base.xml
+++ b/base/src/META-INF/blaze-base.xml
@@ -236,7 +236,7 @@
   <extensionPoints>
     <extensionPoint qualifiedName="com.google.idea.blaze.SyncListener" interface="com.google.idea.blaze.base.sync.SyncListener"/>
     <extensionPoint qualifiedName="com.google.idea.blaze.SyncPlugin" interface="com.google.idea.blaze.base.sync.BlazeSyncPlugin"/>
-    <extensionPoint qualifiedName="com.google.idea.blaze.RuleConfigurationFactory" interface="com.google.idea.blaze.base.run.BlazeRuleConfigurationFactory"/>
+    <extensionPoint qualifiedName="com.google.idea.blaze.RunConfigurationFactory" interface="com.google.idea.blaze.base.run.BlazeRunConfigurationFactory"/>
     <extensionPoint qualifiedName="com.google.idea.blaze.Prefetcher"
                     interface="com.google.idea.blaze.base.prefetch.Prefetcher"/>
     <extensionPoint qualifiedName="com.google.idea.blaze.PrefetchFileSource"
@@ -265,7 +265,6 @@
     <SyncListener implementation="com.google.idea.blaze.base.run.testmap.TestRuleFinderImpl$ClearTestMap"/>
     <SyncListener implementation="com.google.idea.blaze.base.rulemaps.SourceToRuleMapImpl$ClearSourceToTargetMap"/>
     <SyncListener implementation="com.google.idea.blaze.base.lang.buildfile.language.semantics.BuildLanguageSpecProviderImpl"/>
-    <SyncListener implementation="com.google.idea.blaze.base.run.BlazeCommandRunConfigurationUpdater"/>
     <SyncPlugin implementation="com.google.idea.blaze.base.lang.buildfile.sync.BuildLangSyncPlugin"/>
     <BlazeWizardOptionProvider implementation="com.google.idea.blaze.base.wizard2.BazelWizardOptionProvider"/>
     <BuildFlagsProvider implementation="com.google.idea.blaze.base.command.BuildFlagsProviderImpl"/>
@@ -276,6 +275,7 @@
     <BlazeCommandRunConfigurationHandlerProvider implementation="com.google.idea.blaze.base.run.confighandler.BlazeCommandGenericRunConfigurationHandlerProvider" order="last"/>
     <TestRuleHeuristic implementation="com.google.idea.blaze.base.run.RuleNameHeuristic" order="first"/>
     <TestRuleHeuristic implementation="com.google.idea.blaze.base.run.TestSizeHeuristic" order="last" id="TestSizeHeuristic"/>
+    <RunConfigurationFactory implementation="com.google.idea.blaze.base.run.BlazeBuildTargetRunConfigurationFactory" order="last"/>
   </extensions>
 
 </idea-plugin>
diff --git a/base/src/com/google/idea/blaze/base/actions/BlazeCompileFileAction.java b/base/src/com/google/idea/blaze/base/actions/BlazeCompileFileAction.java
index 7a94e64..9c9ed3b 100644
--- a/base/src/com/google/idea/blaze/base/actions/BlazeCompileFileAction.java
+++ b/base/src/com/google/idea/blaze/base/actions/BlazeCompileFileAction.java
@@ -83,7 +83,7 @@
     VirtualFile virtualFile = e.getData(CommonDataKeys.VIRTUAL_FILE);
     if (project != null && virtualFile != null) {
       return SourceToRuleMap.getInstance(project)
-          .getTargetsForSourceFile(new File(virtualFile.getPath()));
+          .getTargetsToBuildForSourceFile(new File(virtualFile.getPath()));
     }
     return ImmutableList.of();
   }
diff --git a/base/src/com/google/idea/blaze/base/buildmap/FileToBuildMap.java b/base/src/com/google/idea/blaze/base/buildmap/FileToBuildMap.java
index 55020f8..b810547 100644
--- a/base/src/com/google/idea/blaze/base/buildmap/FileToBuildMap.java
+++ b/base/src/com/google/idea/blaze/base/buildmap/FileToBuildMap.java
@@ -16,7 +16,6 @@
 package com.google.idea.blaze.base.buildmap;
 
 import com.google.common.collect.ImmutableList;
-import com.google.idea.blaze.base.ideinfo.ArtifactLocation;
 import com.google.idea.blaze.base.model.BlazeProjectData;
 import com.google.idea.blaze.base.rulemaps.SourceToRuleMap;
 import com.google.idea.blaze.base.sync.data.BlazeProjectDataManager;
@@ -46,13 +45,13 @@
       return ImmutableList.of();
     }
     return SourceToRuleMap.getInstance(project)
-        .getTargetsForSourceFile(file)
+        .getRulesForSourceFile(file)
         .stream()
         .map(blazeProjectData.ruleMap::get)
         .filter(Objects::nonNull)
         .map((ruleIdeInfo) -> ruleIdeInfo.buildFile)
         .filter(Objects::nonNull)
-        .map(ArtifactLocation::getFile)
+        .map(blazeProjectData.artifactLocationDecoder::decode)
         .collect(Collectors.toList());
   }
 }
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 f4a84d8..d0d9887 100644
--- a/base/src/com/google/idea/blaze/base/command/BlazeFlags.java
+++ b/base/src/com/google/idea/blaze/base/command/BlazeFlags.java
@@ -113,6 +113,7 @@
     if (!Strings.isNullOrEmpty(methodName)) {
       output.append('#');
       output.append(methodName);
+      output.append('$');
     }
 
     return output.toString();
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 ac2c392..463f4ae 100644
--- a/base/src/com/google/idea/blaze/base/ideinfo/ArtifactLocation.java
+++ b/base/src/com/google/idea/blaze/base/ideinfo/ArtifactLocation.java
@@ -16,32 +16,25 @@
 package com.google.idea.blaze.base.ideinfo;
 
 import com.google.common.base.Objects;
-import java.io.File;
+import com.google.common.collect.ComparisonChain;
 import java.io.Serializable;
 import java.nio.file.Paths;
 
 /** Represents a blaze-produced artifact. */
-public final class ArtifactLocation implements Serializable {
-  private static final long serialVersionUID = 2L;
+public final class ArtifactLocation implements Serializable, Comparable<ArtifactLocation> {
+  private static final long serialVersionUID = 3L;
 
-  public final String rootPath;
   public final String rootExecutionPathFragment;
   public final String relativePath;
   public final boolean isSource;
 
   private ArtifactLocation(
-      String rootPath, String rootExecutionPathFragment, String relativePath, boolean isSource) {
-    this.rootPath = rootPath;
+      String rootExecutionPathFragment, String relativePath, boolean isSource) {
     this.rootExecutionPathFragment = rootExecutionPathFragment;
     this.relativePath = relativePath;
     this.isSource = isSource;
   }
 
-  /** Returns the root path of the artifact, eg. blaze-out */
-  public String getRootPath() {
-    return rootPath;
-  }
-
   /** Gets the path relative to the root path. */
   public String getRelativePath() {
     return relativePath;
@@ -55,10 +48,6 @@
     return !isSource;
   }
 
-  public File getFile() {
-    return new File(getRootPath(), getRelativePath());
-  }
-
   /**
    * Returns rootExecutionPathFragment + relativePath. For source artifacts, this is simply
    * relativePath
@@ -73,16 +62,10 @@
 
   /** Builder for an artifact location */
   public static class Builder {
-    String rootPath;
     String relativePath;
     String rootExecutionPathFragment = "";
     boolean isSource;
 
-    public Builder setRootPath(String rootPath) {
-      this.rootPath = rootPath;
-      return this;
-    }
-
     public Builder setRelativePath(String relativePath) {
       this.relativePath = relativePath;
       return this;
@@ -99,7 +82,7 @@
     }
 
     public ArtifactLocation build() {
-      return new ArtifactLocation(rootPath, rootExecutionPathFragment, relativePath, isSource);
+      return new ArtifactLocation(rootExecutionPathFragment, relativePath, isSource);
     }
   }
 
@@ -112,19 +95,27 @@
       return false;
     }
     ArtifactLocation that = (ArtifactLocation) o;
-    return Objects.equal(rootPath, that.rootPath)
-        && Objects.equal(rootExecutionPathFragment, that.rootExecutionPathFragment)
+    return Objects.equal(rootExecutionPathFragment, that.rootExecutionPathFragment)
         && Objects.equal(relativePath, that.relativePath)
         && Objects.equal(isSource, that.isSource);
   }
 
   @Override
   public int hashCode() {
-    return Objects.hashCode(rootPath, rootExecutionPathFragment, relativePath, isSource);
+    return Objects.hashCode(rootExecutionPathFragment, relativePath, isSource);
   }
 
   @Override
   public String toString() {
-    return getFile().toString();
+    return getExecutionRootRelativePath();
+  }
+
+  @Override
+  public int compareTo(ArtifactLocation o) {
+    return ComparisonChain.start()
+        .compare(rootExecutionPathFragment, o.rootExecutionPathFragment)
+        .compare(relativePath, o.relativePath)
+        .compare(isSource, o.isSource)
+        .result();
   }
 }
diff --git a/base/src/com/google/idea/blaze/base/ideinfo/RuleIdeInfo.java b/base/src/com/google/idea/blaze/base/ideinfo/RuleIdeInfo.java
index a791efc..34bf3ef 100644
--- a/base/src/com/google/idea/blaze/base/ideinfo/RuleIdeInfo.java
+++ b/base/src/com/google/idea/blaze/base/ideinfo/RuleIdeInfo.java
@@ -26,8 +26,9 @@
 
 /** Simple implementation of RuleIdeInfo. */
 public final class RuleIdeInfo implements Serializable {
-  private static final long serialVersionUID = 10L;
+  private static final long serialVersionUID = 12L;
 
+  public final RuleKey key;
   public final Label label;
   public final Kind kind;
   @Nullable public final ArtifactLocation buildFile;
@@ -58,6 +59,7 @@
       @Nullable TestIdeInfo testIdeInfo,
       @Nullable ProtoLibraryLegacyInfo protoLibraryLegacyInfo,
       @Nullable JavaToolchainIdeInfo javaToolchainIdeInfo) {
+    this.key = RuleKey.forPlainTarget(label);
     this.label = label;
     this.kind = kind;
     this.buildFile = buildFile;
@@ -92,6 +94,10 @@
     return false;
   }
 
+  public boolean isPlainTarget() {
+    return true;
+  }
+
   public static Builder builder() {
     return new Builder();
   }
diff --git a/base/src/com/google/idea/blaze/base/ideinfo/RuleKey.java b/base/src/com/google/idea/blaze/base/ideinfo/RuleKey.java
new file mode 100644
index 0000000..8d4cc16
--- /dev/null
+++ b/base/src/com/google/idea/blaze/base/ideinfo/RuleKey.java
@@ -0,0 +1,71 @@
+/*
+ * Copyright 2016 The Bazel Authors. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.idea.blaze.base.ideinfo;
+
+import com.google.common.base.Objects;
+import com.google.idea.blaze.base.model.primitives.Label;
+import java.io.Serializable;
+import java.util.Comparator;
+
+/** A key that uniquely idenfifies a rule in the rule map */
+public class RuleKey implements Serializable, Comparable<RuleKey> {
+  private static final long serialVersionUID = 1L;
+  public static final Comparator<RuleKey> COMPARATOR =
+      (o1, o2) -> String.CASE_INSENSITIVE_ORDER.compare(o1.label.toString(), o2.label.toString());
+
+  public final Label label;
+
+  private RuleKey(Label label) {
+    this.label = label;
+  }
+
+  /** Returns a key identifying dep for a dependency rule -> dep */
+  public static RuleKey forDependency(RuleIdeInfo rule, Label dep) {
+    return new RuleKey(dep);
+  }
+
+  /** Returns a key identifying a plain target */
+  public static RuleKey forPlainTarget(Label label) {
+    return new RuleKey(label);
+  }
+
+  @Override
+  public boolean equals(Object o) {
+    if (this == o) {
+      return true;
+    }
+    if (o == null || getClass() != o.getClass()) {
+      return false;
+    }
+    RuleKey key = (RuleKey) o;
+    return Objects.equal(label, key.label);
+  }
+
+  @Override
+  public int hashCode() {
+    return Objects.hashCode(label);
+  }
+
+  @Override
+  public String toString() {
+    return label.toString();
+  }
+
+  @Override
+  public int compareTo(RuleKey o) {
+    return COMPARATOR.compare(this, o);
+  }
+}
diff --git a/base/src/com/google/idea/blaze/base/model/RuleMap.java b/base/src/com/google/idea/blaze/base/ideinfo/RuleMap.java
similarity index 65%
rename from base/src/com/google/idea/blaze/base/model/RuleMap.java
rename to base/src/com/google/idea/blaze/base/ideinfo/RuleMap.java
index 0a46316..365eaf4 100644
--- a/base/src/com/google/idea/blaze/base/model/RuleMap.java
+++ b/base/src/com/google/idea/blaze/base/ideinfo/RuleMap.java
@@ -13,37 +13,35 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package com.google.idea.blaze.base.model;
+package com.google.idea.blaze.base.ideinfo;
 
 import com.google.common.collect.ImmutableCollection;
 import com.google.common.collect.ImmutableMap;
-import com.google.idea.blaze.base.ideinfo.RuleIdeInfo;
-import com.google.idea.blaze.base.model.primitives.Label;
 import java.io.Serializable;
 
 /** Map of configured targets (and soon aspects). */
 public class RuleMap implements Serializable {
-  private static final long serialVersionUID = 1L;
+  private static final long serialVersionUID = 2L;
 
-  private final ImmutableMap<Label, RuleIdeInfo> ruleMap;
+  private final ImmutableMap<RuleKey, RuleIdeInfo> ruleMap;
 
-  public RuleMap(ImmutableMap<Label, RuleIdeInfo> ruleMap) {
+  public RuleMap(ImmutableMap<RuleKey, RuleIdeInfo> ruleMap) {
     this.ruleMap = ruleMap;
   }
 
-  public RuleIdeInfo get(Label label) {
-    return ruleMap.get(label);
+  public RuleIdeInfo get(RuleKey key) {
+    return ruleMap.get(key);
   }
 
-  public boolean contains(Label label) {
-    return ruleMap.containsKey(label);
+  public boolean contains(RuleKey key) {
+    return ruleMap.containsKey(key);
   }
 
   public ImmutableCollection<RuleIdeInfo> rules() {
     return ruleMap.values();
   }
 
-  public ImmutableMap<Label, RuleIdeInfo> map() {
+  public ImmutableMap<RuleKey, RuleIdeInfo> map() {
     return ruleMap;
   }
 }
diff --git a/base/src/com/google/idea/blaze/base/lang/buildfile/language/semantics/BuildLanguageSpecProviderImpl.java b/base/src/com/google/idea/blaze/base/lang/buildfile/language/semantics/BuildLanguageSpecProviderImpl.java
index 43ea22a..8c7dcb0 100644
--- a/base/src/com/google/idea/blaze/base/lang/buildfile/language/semantics/BuildLanguageSpecProviderImpl.java
+++ b/base/src/com/google/idea/blaze/base/lang/buildfile/language/semantics/BuildLanguageSpecProviderImpl.java
@@ -19,6 +19,7 @@
 import com.google.idea.blaze.base.lang.buildfile.sync.LanguageSpecResult;
 import com.google.idea.blaze.base.model.BlazeProjectData;
 import com.google.idea.blaze.base.projectview.ProjectViewSet;
+import com.google.idea.blaze.base.scope.BlazeContext;
 import com.google.idea.blaze.base.settings.BlazeImportSettings;
 import com.google.idea.blaze.base.sync.SyncListener;
 import com.intellij.openapi.project.Project;
@@ -39,6 +40,7 @@
   @Override
   public void onSyncComplete(
       Project project,
+      BlazeContext context,
       BlazeImportSettings importSettings,
       ProjectViewSet projectViewSet,
       BlazeProjectData blazeProjectData,
diff --git a/base/src/com/google/idea/blaze/base/lang/buildfile/language/semantics/RuleDefinition.java b/base/src/com/google/idea/blaze/base/lang/buildfile/language/semantics/RuleDefinition.java
index 089b598..aa85f8d 100644
--- a/base/src/com/google/idea/blaze/base/lang/buildfile/language/semantics/RuleDefinition.java
+++ b/base/src/com/google/idea/blaze/base/lang/buildfile/language/semantics/RuleDefinition.java
@@ -24,16 +24,23 @@
 /** Simple implementation of RuleDefinition, from build.proto */
 public class RuleDefinition implements Serializable {
 
-  /** This isn't included in the proto -- all other documented attributes seem to be. */
+  /**
+   * In previous versions of blaze/bazel, this wasn't included in the proto. All other documented
+   * attributes seem to be.
+   */
   private static final AttributeDefinition NAME_ATTRIBUTE =
       new AttributeDefinition("name", Build.Attribute.Discriminator.STRING, true, null, null);
 
   public static RuleDefinition fromProto(Build.RuleDefinition rule) {
+    boolean hasNameAttr = false;
     ImmutableMap.Builder<String, AttributeDefinition> map = ImmutableMap.builder();
     for (Build.AttributeDefinition attr : rule.getAttributeList()) {
       map.put(attr.getName(), AttributeDefinition.fromProto(attr));
+      hasNameAttr |= "name".equals(attr.getName());
     }
-    map.put(NAME_ATTRIBUTE.name, NAME_ATTRIBUTE);
+    if (!hasNameAttr) {
+      map.put(NAME_ATTRIBUTE.name, NAME_ATTRIBUTE);
+    }
     return new RuleDefinition(
         rule.getName(), map.build(), rule.hasDocumentation() ? rule.getDocumentation() : null);
   }
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 5bd69c7..69b1b77 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
@@ -18,8 +18,8 @@
 import com.google.common.collect.ImmutableList;
 import com.google.common.util.concurrent.ListenableFuture;
 import com.google.idea.blaze.base.command.info.BlazeInfo;
+import com.google.idea.blaze.base.ideinfo.RuleMap;
 import com.google.idea.blaze.base.lang.buildfile.language.semantics.BuildLanguageSpec;
-import com.google.idea.blaze.base.model.RuleMap;
 import com.google.idea.blaze.base.model.SyncState;
 import com.google.idea.blaze.base.model.primitives.WorkspaceRoot;
 import com.google.idea.blaze.base.projectview.ProjectViewSet;
@@ -29,6 +29,7 @@
 import com.google.idea.blaze.base.settings.Blaze;
 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;
@@ -55,6 +56,7 @@
       BlazeRoots blazeRoots,
       @Nullable WorkingSet workingSet,
       WorkspacePathResolver workspacePathResolver,
+      ArtifactLocationDecoder artifactLocationDecoder,
       RuleMap ruleMap,
       SyncState.Builder syncStateBuilder,
       @Nullable SyncState previousSyncState) {
diff --git a/base/src/com/google/idea/blaze/base/metrics/Action.java b/base/src/com/google/idea/blaze/base/metrics/Action.java
index 9292cdc..89843df 100644
--- a/base/src/com/google/idea/blaze/base/metrics/Action.java
+++ b/base/src/com/google/idea/blaze/base/metrics/Action.java
@@ -48,7 +48,7 @@
   BLAZE_COMMAND_USAGE("ttrpbc"),
 
   OPEN_IN_CODESEARCH("oics"),
-  COPY_GOOGLE3_PATH("cg3p"),
+  COPY_DEPOT_PATH("cg3p"),
   OPEN_CORRESPONDING_BUILD_FILE("ocbf"),
 
   CREATE_BLAZE_RULE("cbr"),
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 d4e84f7..8a2f278 100644
--- a/base/src/com/google/idea/blaze/base/model/BlazeProjectData.java
+++ b/base/src/com/google/idea/blaze/base/model/BlazeProjectData.java
@@ -16,8 +16,10 @@
 package com.google.idea.blaze.base.model;
 
 import com.google.common.collect.ImmutableMultimap;
-import com.google.idea.blaze.base.model.primitives.Label;
+import com.google.idea.blaze.base.ideinfo.RuleKey;
+import com.google.idea.blaze.base.ideinfo.RuleMap;
 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;
@@ -28,16 +30,17 @@
 /** The top-level object serialized to cache. */
 @Immutable
 public class BlazeProjectData implements Serializable {
-  private static final long serialVersionUID = 21L;
+  private static final long serialVersionUID = 23L;
 
   public final long syncTime;
   public final RuleMap ruleMap;
   public final BlazeRoots blazeRoots;
   @Nullable public final WorkingSet workingSet;
   public final WorkspacePathResolver workspacePathResolver;
+  public final ArtifactLocationDecoder artifactLocationDecoder;
   public final WorkspaceLanguageSettings workspaceLanguageSettings;
   public final SyncState syncState;
-  public final ImmutableMultimap<Label, Label> reverseDependencies;
+  public final ImmutableMultimap<RuleKey, RuleKey> reverseDependencies;
   @Nullable public final String vcsName;
 
   public BlazeProjectData(
@@ -46,15 +49,17 @@
       BlazeRoots blazeRoots,
       @Nullable WorkingSet workingSet,
       WorkspacePathResolver workspacePathResolver,
+      ArtifactLocationDecoder artifactLocationDecoder,
       WorkspaceLanguageSettings workspaceLangaugeSettings,
       SyncState syncState,
-      ImmutableMultimap<Label, Label> reverseDependencies,
+      ImmutableMultimap<RuleKey, RuleKey> reverseDependencies,
       String vcsName) {
     this.syncTime = syncTime;
     this.ruleMap = ruleMap;
     this.blazeRoots = blazeRoots;
     this.workingSet = workingSet;
     this.workspacePathResolver = workspacePathResolver;
+    this.artifactLocationDecoder = artifactLocationDecoder;
     this.workspaceLanguageSettings = workspaceLangaugeSettings;
     this.syncState = syncState;
     this.reverseDependencies = reverseDependencies;
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 96bf0ab..80186c9 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
@@ -45,6 +45,7 @@
   GWT_HOST("gwt_host", LanguageClass.JAVA),
   GWT_MODULE("gwt_module", LanguageClass.JAVA),
   GWT_TEST("gwt_test", LanguageClass.JAVA),
+  TEST_SUITE("test_suite", LanguageClass.GENERIC),
   ;
 
   static final ImmutableMap<String, Kind> STRING_TO_KIND = makeStringToKindMap();
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 532c347..302a27d 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
@@ -19,7 +19,6 @@
 import com.google.idea.blaze.base.ui.BlazeValidationError;
 import com.intellij.openapi.diagnostic.Logger;
 import java.util.Collection;
-import java.util.Comparator;
 import java.util.List;
 import javax.annotation.Nullable;
 import javax.annotation.concurrent.Immutable;
@@ -29,9 +28,6 @@
 public final class Label extends TargetExpression {
   private static final Logger LOG = Logger.getInstance(Label.class);
 
-  public static final Comparator<Label> COMPARATOR =
-      (o1, o2) -> String.CASE_INSENSITIVE_ORDER.compare(o1.toString(), o2.toString());
-
   public static final long serialVersionUID = 2L;
 
   /** Silently returns null if this is not a valid Label */
diff --git a/base/src/com/google/idea/blaze/base/rulemaps/ReverseDependencyMap.java b/base/src/com/google/idea/blaze/base/rulemaps/ReverseDependencyMap.java
index 0a55a8e..1941eea 100644
--- a/base/src/com/google/idea/blaze/base/rulemaps/ReverseDependencyMap.java
+++ b/base/src/com/google/idea/blaze/base/rulemaps/ReverseDependencyMap.java
@@ -18,18 +18,20 @@
 import com.google.common.collect.ImmutableMultimap;
 import com.google.common.collect.Iterables;
 import com.google.idea.blaze.base.ideinfo.RuleIdeInfo;
-import com.google.idea.blaze.base.model.RuleMap;
+import com.google.idea.blaze.base.ideinfo.RuleKey;
+import com.google.idea.blaze.base.ideinfo.RuleMap;
 import com.google.idea.blaze.base.model.primitives.Label;
 
 /** Handy class to create an reverse dep map of all rules */
 public class ReverseDependencyMap {
-  public static ImmutableMultimap<Label, Label> createRdepsMap(RuleMap ruleMap) {
-    ImmutableMultimap.Builder<Label, Label> builder = ImmutableMultimap.builder();
+  public static ImmutableMultimap<RuleKey, RuleKey> createRdepsMap(RuleMap ruleMap) {
+    ImmutableMultimap.Builder<RuleKey, RuleKey> builder = ImmutableMultimap.builder();
     for (RuleIdeInfo rule : ruleMap.rules()) {
-      Label label = rule.label;
+      RuleKey key = rule.key;
       for (Label dep : Iterables.concat(rule.dependencies, rule.runtimeDeps)) {
-        if (ruleMap.contains(dep)) {
-          builder.put(dep, label);
+        RuleKey depKey = RuleKey.forDependency(rule, dep);
+        if (ruleMap.contains(depKey)) {
+          builder.put(depKey, key);
         }
       }
     }
diff --git a/base/src/com/google/idea/blaze/base/rulemaps/SourceToRuleMap.java b/base/src/com/google/idea/blaze/base/rulemaps/SourceToRuleMap.java
index 6fcf01e..df650ea 100644
--- a/base/src/com/google/idea/blaze/base/rulemaps/SourceToRuleMap.java
+++ b/base/src/com/google/idea/blaze/base/rulemaps/SourceToRuleMap.java
@@ -16,6 +16,7 @@
 package com.google.idea.blaze.base.rulemaps;
 
 import com.google.common.collect.ImmutableCollection;
+import com.google.idea.blaze.base.ideinfo.RuleKey;
 import com.google.idea.blaze.base.model.primitives.Label;
 import com.intellij.openapi.components.ServiceManager;
 import com.intellij.openapi.project.Project;
@@ -28,5 +29,9 @@
     return ServiceManager.getService(project, SourceToRuleMap.class);
   }
 
-  ImmutableCollection<Label> getTargetsForSourceFile(File file);
+  /** Returns a set of targets that will cause the file to build */
+  ImmutableCollection<Label> getTargetsToBuildForSourceFile(File file);
+
+  /** Returns the rules that contain a given source file */
+  ImmutableCollection<RuleKey> getRulesForSourceFile(File file);
 }
diff --git a/base/src/com/google/idea/blaze/base/rulemaps/SourceToRuleMapImpl.java b/base/src/com/google/idea/blaze/base/rulemaps/SourceToRuleMapImpl.java
index 56fd32f..af86d33 100644
--- a/base/src/com/google/idea/blaze/base/rulemaps/SourceToRuleMapImpl.java
+++ b/base/src/com/google/idea/blaze/base/rulemaps/SourceToRuleMapImpl.java
@@ -20,21 +20,26 @@
 import com.google.common.collect.ImmutableMultimap;
 import com.google.idea.blaze.base.ideinfo.ArtifactLocation;
 import com.google.idea.blaze.base.ideinfo.RuleIdeInfo;
+import com.google.idea.blaze.base.ideinfo.RuleKey;
 import com.google.idea.blaze.base.model.BlazeProjectData;
 import com.google.idea.blaze.base.model.primitives.Label;
 import com.google.idea.blaze.base.projectview.ProjectViewSet;
+import com.google.idea.blaze.base.scope.BlazeContext;
 import com.google.idea.blaze.base.settings.BlazeImportSettings;
 import com.google.idea.blaze.base.sync.SyncListener;
 import com.google.idea.blaze.base.sync.data.BlazeProjectDataManager;
+import com.google.idea.blaze.base.sync.workspace.ArtifactLocationDecoder;
 import com.intellij.openapi.components.ServiceManager;
 import com.intellij.openapi.project.Project;
 import java.io.File;
+import java.util.Objects;
+import java.util.stream.Collectors;
 import javax.annotation.Nullable;
 
 /** Maps source files to their respective targets */
 public class SourceToRuleMapImpl implements SourceToRuleMap {
   private final Project project;
-  private ImmutableMultimap<File, Label> sourceToTargetMap;
+  private ImmutableMultimap<File, RuleKey> sourceToTargetMap;
 
   public static SourceToRuleMapImpl getImpl(Project project) {
     return (SourceToRuleMapImpl) ServiceManager.getService(project, SourceToRuleMap.class);
@@ -45,13 +50,35 @@
   }
 
   @Override
-  public ImmutableCollection<Label> getTargetsForSourceFile(File file) {
-    ImmutableMultimap<File, Label> sourceToTargetMap = getSourceToTargetMap();
-    return sourceToTargetMap != null ? sourceToTargetMap.get(file) : ImmutableList.of();
+  public ImmutableCollection<Label> getTargetsToBuildForSourceFile(File sourceFile) {
+    BlazeProjectData blazeProjectData =
+        BlazeProjectDataManager.getInstance(project).getBlazeProjectData();
+    if (blazeProjectData == null) {
+      return ImmutableList.of();
+    }
+    return ImmutableList.copyOf(
+        getRulesForSourceFile(sourceFile)
+            .stream()
+            .map(blazeProjectData.ruleMap::get)
+            .filter(Objects::nonNull)
+            // TODO(tomlu): For non-plain targets we need to rdep our way back to a target to build
+            // Without this, you won't be able to invoke "build" on (say) a proto_library
+            .filter(RuleIdeInfo::isPlainTarget)
+            .map(rule -> rule.label)
+            .collect(Collectors.toList()));
+  }
+
+  @Override
+  public ImmutableCollection<RuleKey> getRulesForSourceFile(File sourceFile) {
+    ImmutableMultimap<File, RuleKey> sourceToTargetMap = getSourceToTargetMap();
+    if (sourceToTargetMap == null) {
+      return ImmutableList.of();
+    }
+    return sourceToTargetMap.get(sourceFile);
   }
 
   @Nullable
-  private synchronized ImmutableMultimap<File, Label> getSourceToTargetMap() {
+  private synchronized ImmutableMultimap<File, RuleKey> getSourceToTargetMap() {
     if (this.sourceToTargetMap == null) {
       this.sourceToTargetMap = initSourceToTargetMap();
     }
@@ -63,17 +90,18 @@
   }
 
   @Nullable
-  private ImmutableMultimap<File, Label> initSourceToTargetMap() {
+  private ImmutableMultimap<File, RuleKey> initSourceToTargetMap() {
     BlazeProjectData blazeProjectData =
         BlazeProjectDataManager.getInstance(project).getBlazeProjectData();
     if (blazeProjectData == null) {
       return null;
     }
-    ImmutableMultimap.Builder<File, Label> sourceToTargetMap = ImmutableMultimap.builder();
+    ArtifactLocationDecoder artifactLocationDecoder = blazeProjectData.artifactLocationDecoder;
+    ImmutableMultimap.Builder<File, RuleKey> sourceToTargetMap = ImmutableMultimap.builder();
     for (RuleIdeInfo rule : blazeProjectData.ruleMap.rules()) {
-      Label label = rule.label;
+      RuleKey key = rule.key;
       for (ArtifactLocation sourceArtifact : rule.sources) {
-        sourceToTargetMap.put(sourceArtifact.getFile(), label);
+        sourceToTargetMap.put(artifactLocationDecoder.decode(sourceArtifact), key);
       }
     }
     return sourceToTargetMap.build();
@@ -83,6 +111,7 @@
     @Override
     public void onSyncComplete(
         Project project,
+        BlazeContext context,
         BlazeImportSettings importSettings,
         ProjectViewSet projectViewSet,
         BlazeProjectData blazeProjectData,
diff --git a/base/src/com/google/idea/blaze/base/run/BlazeBeforeRunTaskProvider.java b/base/src/com/google/idea/blaze/base/run/BlazeBeforeRunTaskProvider.java
index aa9d617..7eb51e3 100644
--- a/base/src/com/google/idea/blaze/base/run/BlazeBeforeRunTaskProvider.java
+++ b/base/src/com/google/idea/blaze/base/run/BlazeBeforeRunTaskProvider.java
@@ -15,6 +15,7 @@
  */
 package com.google.idea.blaze.base.run;
 
+import com.google.idea.blaze.base.run.confighandler.BlazeCommandRunConfigurationRunner;
 import com.google.idea.blaze.base.settings.Blaze;
 import com.intellij.execution.BeforeRunTask;
 import com.intellij.execution.BeforeRunTaskProvider;
@@ -102,7 +103,8 @@
     if (!canExecuteTask(configuration, task)) {
       return false;
     }
-    BlazeCommandRunConfiguration config = (BlazeCommandRunConfiguration) configuration;
-    return config.getHandler().executeBeforeRunTask(env);
+    BlazeCommandRunConfigurationRunner runner =
+        env.getCopyableUserData(BlazeCommandRunConfigurationRunner.RUNNER_KEY);
+    return runner.executeBeforeRunTask(env);
   }
 }
diff --git a/base/src/com/google/idea/blaze/base/run/BlazeBuildTargetRunConfigurationFactory.java b/base/src/com/google/idea/blaze/base/run/BlazeBuildTargetRunConfigurationFactory.java
new file mode 100644
index 0000000..61749cf
--- /dev/null
+++ b/base/src/com/google/idea/blaze/base/run/BlazeBuildTargetRunConfigurationFactory.java
@@ -0,0 +1,45 @@
+/*
+ * Copyright 2016 The Bazel Authors. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.idea.blaze.base.run;
+
+import com.google.idea.blaze.base.model.BlazeProjectData;
+import com.google.idea.blaze.base.model.primitives.Label;
+import com.google.idea.blaze.base.run.producers.BlazeBuildFileRunConfigurationProducer;
+import com.intellij.execution.configurations.ConfigurationFactory;
+import com.intellij.execution.configurations.RunConfiguration;
+import com.intellij.openapi.project.Project;
+
+/**
+ * A factory creating run configurations based on BUILD file targets. Runs last, as a fallback for
+ * the case where no more specialized factory handles the target.
+ */
+public class BlazeBuildTargetRunConfigurationFactory extends BlazeRunConfigurationFactory {
+
+  @Override
+  public boolean handlesTarget(Project project, BlazeProjectData blazeProjectData, Label target) {
+    return BlazeBuildFileRunConfigurationProducer.handlesTarget(project, target);
+  }
+
+  @Override
+  protected ConfigurationFactory getConfigurationFactory() {
+    return BlazeCommandRunConfigurationType.getInstance().getFactory();
+  }
+
+  @Override
+  public void setupConfiguration(RunConfiguration configuration, Label target) {
+    BlazeBuildFileRunConfigurationProducer.setupConfiguration(configuration, target);
+  }
+}
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 4d53ff5..5405d02 100644
--- a/base/src/com/google/idea/blaze/base/run/BlazeCommandRunConfiguration.java
+++ b/base/src/com/google/idea/blaze/base/run/BlazeCommandRunConfiguration.java
@@ -21,16 +21,16 @@
 import com.google.idea.blaze.base.model.primitives.Label;
 import com.google.idea.blaze.base.model.primitives.TargetExpression;
 import com.google.idea.blaze.base.run.confighandler.BlazeCommandRunConfigurationHandler;
-import com.google.idea.blaze.base.run.confighandler.BlazeCommandRunConfigurationHandlerEditor;
 import com.google.idea.blaze.base.run.confighandler.BlazeCommandRunConfigurationHandlerProvider;
-import com.google.idea.blaze.base.run.confighandler.BlazeUnknownRunConfigurationHandler;
+import com.google.idea.blaze.base.run.confighandler.BlazeCommandRunConfigurationRunner;
 import com.google.idea.blaze.base.run.rulefinder.RuleFinder;
+import com.google.idea.blaze.base.run.state.RunConfigurationState;
+import com.google.idea.blaze.base.run.state.RunConfigurationStateEditor;
 import com.google.idea.blaze.base.settings.Blaze;
 import com.google.idea.blaze.base.sync.data.BlazeProjectDataManager;
 import com.google.idea.blaze.base.ui.UiUtil;
 import com.intellij.execution.ExecutionException;
 import com.intellij.execution.Executor;
-import com.intellij.execution.RunManager;
 import com.intellij.execution.RunnerIconProvider;
 import com.intellij.execution.configurations.ConfigurationFactory;
 import com.intellij.execution.configurations.LocatableConfigurationBase;
@@ -47,12 +47,14 @@
 import com.intellij.ui.components.JBLabel;
 import com.intellij.ui.components.JBTextField;
 import com.intellij.util.ui.UIUtil;
+import java.util.Set;
+import java.util.stream.Collectors;
 import javax.annotation.Nullable;
 import javax.swing.Box;
 import javax.swing.Icon;
 import javax.swing.JComponent;
+import org.jdom.Attribute;
 import org.jdom.Element;
-import org.jetbrains.annotations.NotNull;
 
 /** A run configuration which executes Blaze commands. */
 public class BlazeCommandRunConfiguration extends LocatableConfigurationBase
@@ -63,35 +65,41 @@
   private static final String TARGET_TAG = "blaze-target";
   private static final String KIND_ATTR = "kind";
 
-  // Null for configurations created since restart.
-  @Nullable private Element externalElementBackup;
-  // Null when there is no target.
+  /** The last serialized state of the configuration. */
+  private Element elementState = new Element("dummy");
+
   @Nullable private TargetExpression target;
   // Null if the target is null, not a Label, or not a known rule.
   @Nullable private Kind targetKind;
+  private BlazeCommandRunConfigurationHandlerProvider handlerProvider;
   private BlazeCommandRunConfigurationHandler handler;
-  // Null if the handler is BlazeUnknownRunConfigurationHandler.
-  @Nullable private BlazeCommandRunConfigurationHandlerProvider handlerProvider;
 
   public BlazeCommandRunConfiguration(Project project, ConfigurationFactory factory, String name) {
     super(project, factory, name);
-    handler = new BlazeUnknownRunConfigurationHandler(this);
+    // start with whatever fallback is present
+    handlerProvider = BlazeCommandRunConfigurationHandlerProvider.findHandlerProvider(null);
+    handler = handlerProvider.createHandler(this);
+    try {
+      handler.getState().readExternal(elementState);
+    } catch (InvalidDataException e) {
+      LOG.error(e);
+    }
   }
 
   /** @return The configuration's {@link BlazeCommandRunConfigurationHandler}. */
-  @NotNull
   public BlazeCommandRunConfigurationHandler getHandler() {
     return handler;
   }
 
   /**
-   * Gets the configuration's {@link BlazeCommandRunConfigurationHandler} if it is an instance of
-   * the given class; otherwise returns null.
+   * Gets the configuration's handler's {@link RunConfigurationState} if it is an instance of the
+   * given class; otherwise returns null.
    */
   @Nullable
-  public <T extends BlazeCommandRunConfigurationHandler> T getHandlerIfType(Class<T> type) {
-    if (type.isInstance(handler)) {
-      return type.cast(handler);
+  public <T extends RunConfigurationState> T getHandlerStateIfType(Class<T> type) {
+    RunConfigurationState handlerState = handler.getState();
+    if (type.isInstance(handlerState)) {
+      return type.cast(handlerState);
     } else {
       return null;
     }
@@ -105,32 +113,42 @@
 
   public void setTarget(@Nullable TargetExpression target) {
     this.target = target;
-    RuleIdeInfo rule = getRuleForTarget();
-    targetKind = rule != null ? rule.kind : null;
+    targetKind = getKindForTarget();
 
     BlazeCommandRunConfigurationHandlerProvider handlerProvider =
         BlazeCommandRunConfigurationHandlerProvider.findHandlerProvider(targetKind);
-    setHandlerIfDifferentProvider(handlerProvider);
+    updateHandlerIfDifferentProvider(handlerProvider);
   }
 
-  private void setHandlerIfDifferentProvider(
+  private void updateHandlerIfDifferentProvider(
       BlazeCommandRunConfigurationHandlerProvider newProvider) {
-    // Only change the handler if the provider has changed.
-    if (handlerProvider != newProvider) {
-      handlerProvider = newProvider;
-      handler = newProvider.createHandler(this);
+    if (handlerProvider == newProvider) {
+      return;
+    }
+    try {
+      handler.getState().writeExternal(elementState);
+    } catch (WriteExternalException e) {
+      LOG.error(e);
+    }
+    handlerProvider = newProvider;
+    handler = newProvider.createHandler(this);
+    try {
+      handler.getState().readExternal(elementState);
+    } catch (InvalidDataException e) {
+      LOG.error(e);
     }
   }
 
   /**
-   * Returns the single blaze target corresponding to the configuration's target expression, if one
-   * exists. Returns null if the target expression points to multiple blaze targets, or wasn't
-   * included in the latest sync.
+   * Returns the {@link Kind} of the single blaze target corresponding to the configuration's target
+   * expression, if it can be determined. Returns null if the target expression points to multiple
+   * blaze targets.
    */
   @Nullable
-  public RuleIdeInfo getRuleForTarget() {
+  public Kind getKindForTarget() {
     if (target instanceof Label) {
-      return RuleFinder.getInstance().ruleForTarget(getProject(), (Label) target);
+      RuleIdeInfo rule = RuleFinder.getInstance().ruleForTarget(getProject(), (Label) target);
+      return rule != null ? rule.kind : null;
     }
     return null;
   }
@@ -141,9 +159,9 @@
    *     known rule, and "unknown target" if there is no target.
    */
   public String getTargetKindName() {
-    RuleIdeInfo rule = getRuleForTarget();
-    if (rule != null) {
-      return rule.kind.toString();
+    Kind kind = getKindForTarget();
+    if (kind != null) {
+      return kind.toString();
     } else if (target instanceof Label) {
       return "unknown rule";
     } else if (target != null) {
@@ -153,36 +171,12 @@
     }
   }
 
-  // TODO This method can be private after BlazeCommandRunConfigurationUpdater is removed.
-  void loadExternalElementBackup() {
-    if (externalElementBackup != null) {
-      try {
-        handler.readExternal(externalElementBackup);
-      } catch (InvalidDataException e) {
-        // This is what IntelliJ does when getting this exception while loading a configuration.
-        LOG.error(e);
-      }
-    }
-  }
-
   @Override
   public void checkConfiguration() throws RuntimeConfigurationException {
-    // Our handler check and its quick fix are not valid when we don't have BlazeProjectData.
+    // Our handler check is not valid when we don't have BlazeProjectData.
     if (BlazeProjectDataManager.getInstance(getProject()).getBlazeProjectData() == null) {
       throw new RuntimeConfigurationError(
-          "Configuration cannot be used or modified while project is syncing.");
-    }
-    if (isConfigurationInvalidated()) {
-      throw new RuntimeConfigurationError(
-          "A property of the target unexpectedly changed. The configuration must be updated. "
-              + "Some configuration settings may be lost.",
-          () -> {
-            BlazeCommandRunConfigurationHandler oldHandler = handler;
-            setTarget(target);
-            if (handler != oldHandler) {
-              loadExternalElementBackup();
-            }
-          });
+          "Configuration cannot be run until project has been synced.");
     }
     if (target == null) {
       throw new RuntimeConfigurationError(
@@ -196,25 +190,11 @@
     handler.checkConfiguration();
   }
 
-  private boolean isConfigurationInvalidated() {
-    boolean configurationInvalidated = handler instanceof BlazeUnknownRunConfigurationHandler;
-    if (!configurationInvalidated) {
-      RuleIdeInfo rule = getRuleForTarget();
-      Kind expectedKind = rule != null ? rule.kind : null;
-      configurationInvalidated = targetKind != expectedKind;
-    }
-    if (!configurationInvalidated) {
-      configurationInvalidated =
-          handlerProvider
-              != BlazeCommandRunConfigurationHandlerProvider.findHandlerProvider(targetKind);
-    }
-    return configurationInvalidated;
-  }
-
   @Override
   public void readExternal(Element element) throws InvalidDataException {
     super.readExternal(element);
-    externalElementBackup = element.clone();
+    element = element.clone();
+
     // Target is persisted as a tag to permit multiple targets in the future.
     Element targetElement = element.getChild(TARGET_TAG);
     if (targetElement != null && !Strings.isNullOrEmpty(targetElement.getTextTrim())) {
@@ -225,16 +205,8 @@
       // BlazeAndroid(Binary/Test)RunConfiguration elements can be read.
       // TODO remove in 2.1 once BlazeAndroidBinaryRunConfigurationType and
       // BlazeAndroidTestRunConfigurationType have been removed.
-      String targetString =
-          element.getAttributeValue(
-              TARGET_TAG); // The attribute ID happens to be identical to the tag ID.
-      if (targetString != null) {
-        target = TargetExpression.fromString(targetString);
-        // Once the above is removed, 'target = null;' should be
-        // the only thing in the outer else clause.
-      } else {
-        target = null;
-      }
+      String targetString = element.getAttributeValue(TARGET_TAG);
+      target = targetString != null ? TargetExpression.fromString(targetString) : null;
     }
     // Because BlazeProjectData is not available when configurations are loading,
     // we can't call setTarget and have it find the appropriate handler provider.
@@ -243,21 +215,24 @@
     BlazeCommandRunConfigurationHandlerProvider handlerProvider =
         BlazeCommandRunConfigurationHandlerProvider.getHandlerProvider(providerId);
     if (handlerProvider != null) {
-      setHandlerIfDifferentProvider(handlerProvider);
+      updateHandlerIfDifferentProvider(handlerProvider);
     }
-    handler.readExternal(element);
+
+    element.removeAttribute(KIND_ATTR);
+    element.removeAttribute(HANDLER_ATTR);
+    element.removeChildren(TARGET_TAG);
+    // remove legacy attribute, if present
+    element.removeAttribute(TARGET_TAG);
+
+    this.elementState = element;
+    handler.getState().readExternal(elementState);
   }
 
   @Override
   @SuppressWarnings("ThrowsUncheckedException")
   public void writeExternal(Element element) throws WriteExternalException {
     super.writeExternal(element);
-    // We can't write externalElementBackup contents; doing so would cause the configuration
-    // xml to retain duplicate elements and grow across reopenings.
-    // We also can't use the approach in BlazeUnknownRunConfigurationHandler;
-    // this can revive intentionally deleted attributes/elements such as user flags.
     if (target != null) {
-      // Target is persisted as a tag to permit multiple targets in the future.
       Element targetElement = new Element(TARGET_TAG);
       targetElement.setText(target.toString());
       if (targetKind != null) {
@@ -265,95 +240,92 @@
       }
       element.addContent(targetElement);
     }
-    if (handlerProvider != null) {
-      element.setAttribute(HANDLER_ATTR, handlerProvider.getId());
+    element.setAttribute(HANDLER_ATTR, handlerProvider.getId());
+    handler.getState().writeExternal(elementState);
+
+    // copy our internal state to the provided Element, skipping items already present
+    Set<String> baseAttributes =
+        element.getAttributes().stream().map(Attribute::getName).collect(Collectors.toSet());
+    for (Attribute attribute : elementState.getAttributes()) {
+      if (!baseAttributes.contains(attribute.getName())) {
+        element.setAttribute(attribute.clone());
+      }
     }
-    handler.writeExternal(element);
+    Set<String> baseChildren =
+        element.getChildren().stream().map(Element::getName).collect(Collectors.toSet());
+    for (Element child : elementState.getChildren()) {
+      if (!baseChildren.contains(child.getName())) {
+        element.addContent(child.clone());
+      }
+    }
   }
 
   @Override
   public BlazeCommandRunConfiguration clone() {
     final BlazeCommandRunConfiguration configuration = (BlazeCommandRunConfiguration) super.clone();
-    if (externalElementBackup != null) {
-      configuration.externalElementBackup = externalElementBackup.clone();
-    }
+    configuration.elementState = elementState.clone();
     configuration.target = target;
     configuration.targetKind = targetKind;
-    configuration.handler = handler.cloneFor(configuration);
     configuration.handlerProvider = handlerProvider;
+    configuration.handler = handlerProvider.createHandler(this);
+    try {
+      configuration.handler.getState().readExternal(configuration.elementState);
+    } catch (InvalidDataException e) {
+      LOG.error(e);
+    }
+
     return configuration;
   }
 
   @Override
   @Nullable
-  public RunProfileState getState(
-      @NotNull Executor executor, @NotNull ExecutionEnvironment environment)
+  public RunProfileState getState(Executor executor, ExecutionEnvironment environment)
       throws ExecutionException {
-    return handler.getState(executor, environment);
+    BlazeCommandRunConfigurationRunner runner = handler.createRunner(executor, environment);
+    if (runner != null) {
+      environment.putCopyableUserData(BlazeCommandRunConfigurationRunner.RUNNER_KEY, runner);
+      return runner.getRunProfileState(executor, environment);
+    }
+    return null;
   }
 
   @Override
   @Nullable
   public String suggestedName() {
-    return handler.suggestedName();
-  }
-
-  @Override
-  public boolean isGeneratedName() {
-    return handler.isGeneratedName(super.isGeneratedName());
+    return handler.suggestedName(this);
   }
 
   @Override
   @Nullable
-  public Icon getExecutorIcon(@NotNull RunConfiguration configuration, @NotNull Executor executor) {
+  public Icon getExecutorIcon(RunConfiguration configuration, Executor executor) {
     return handler.getExecutorIcon(configuration, executor);
   }
 
   @Override
-  @NotNull
   public SettingsEditor<? extends BlazeCommandRunConfiguration> getConfigurationEditor() {
     return new BlazeCommandRunConfigurationSettingsEditor(this);
   }
 
   static class BlazeCommandRunConfigurationSettingsEditor
       extends SettingsEditor<BlazeCommandRunConfiguration> {
-    @Nullable private BlazeCommandRunConfigurationHandlerProvider handlerProvider;
-    private BlazeCommandRunConfigurationHandlerEditor handlerEditor;
-    @Nullable private JComponent handlerComponent;
+
+    private BlazeCommandRunConfigurationHandlerProvider handlerProvider;
+    private BlazeCommandRunConfigurationHandler handler;
+    private RunConfigurationStateEditor handlerStateEditor;
+    private JComponent handlerStateComponent;
+    private Element elementState;
 
     private final Box editor;
     private final JBLabel targetExpressionLabel;
     private final JBTextField targetField = new JBTextField(1);
 
-    private boolean isEditable;
-
-    public BlazeCommandRunConfigurationSettingsEditor(BlazeCommandRunConfiguration config) {
+    BlazeCommandRunConfigurationSettingsEditor(BlazeCommandRunConfiguration config) {
+      elementState = config.elementState.clone();
       targetExpressionLabel = new JBLabel(UIUtil.ComponentStyle.LARGE);
       editor = UiUtil.createBox(targetExpressionLabel, targetField);
       targetField.getEmptyText().setText("Full target expression starting with //");
       updateTargetExpressionLabel(config);
       updateHandlerEditor(config);
-      setEditable(isConfigurationEditable(config));
-    }
-
-    private static boolean isConfigurationEditable(BlazeCommandRunConfiguration config) {
-      RunConfiguration template =
-          RunManager.getInstance(config.getProject())
-              .getConfigurationTemplate(config.getFactory())
-              .getConfiguration();
-      if (config == template) {
-        return true; // The default template is always editable.
-      }
-      return BlazeProjectDataManager.getInstance(config.getProject()).getBlazeProjectData() != null
-          && !config.isConfigurationInvalidated();
-    }
-
-    private void setEditable(boolean editable) {
-      isEditable = editable;
-      targetField.setEnabled(isEditable);
-      if (handlerComponent != null) {
-        handlerComponent.setVisible(isEditable);
-      }
     }
 
     private void updateTargetExpressionLabel(BlazeCommandRunConfiguration config) {
@@ -365,56 +337,66 @@
 
     private void updateHandlerEditor(BlazeCommandRunConfiguration config) {
       handlerProvider = config.handlerProvider;
-      handlerEditor = config.handler.getHandlerEditor();
+      handler = handlerProvider.createHandler(config);
+      try {
+        handler.getState().readExternal(config.elementState);
+      } catch (InvalidDataException e) {
+        LOG.error(e);
+      }
+      handlerStateEditor = handler.getState().getEditor(config.getProject());
 
-      if (handlerComponent != null) {
-        editor.remove(handlerComponent);
+      if (handlerStateComponent != null) {
+        editor.remove(handlerStateComponent);
       }
-      handlerComponent = handlerEditor.createEditor();
-      if (handlerComponent != null) {
-        editor.add(handlerComponent);
-      }
+      handlerStateComponent = handlerStateEditor.createComponent();
+      editor.add(handlerStateComponent);
     }
 
     @Override
-    @NotNull
     protected JComponent createEditor() {
       return editor;
     }
 
     @Override
     protected void resetEditorFrom(BlazeCommandRunConfiguration config) {
+      elementState = config.elementState.clone();
       updateTargetExpressionLabel(config);
       if (config.handlerProvider != handlerProvider) {
         updateHandlerEditor(config);
       }
-      setEditable(isConfigurationEditable(config));
       targetField.setText(config.target == null ? null : config.target.toString());
-      handlerEditor.resetEditorFrom(config.handler);
+      handlerStateEditor.resetEditorFrom(config.handler.getState());
     }
 
     @Override
     protected void applyEditorTo(BlazeCommandRunConfiguration config) {
-      if (!isEditable) {
-        return;
+      // update the editor's elementState
+      handlerStateEditor.applyEditorTo(handler.getState());
+      try {
+        handler.getState().writeExternal(elementState);
+      } catch (WriteExternalException e) {
+        LOG.error(e);
       }
-      applyTarget(config);
+
+      // now set the config's state, based on the editor's (possibly out of date) handler
+      config.updateHandlerIfDifferentProvider(handlerProvider);
+      config.elementState = elementState.clone();
+      try {
+        config.handler.getState().readExternal(config.elementState);
+      } catch (InvalidDataException e) {
+        LOG.error(e);
+      }
+
+      // finally, update the handler
+      String targetString = targetField.getText();
+      config.setTarget(
+          Strings.isNullOrEmpty(targetString) ? null : TargetExpression.fromString(targetString));
       updateTargetExpressionLabel(config);
       if (config.handlerProvider != handlerProvider) {
         updateHandlerEditor(config);
-        handlerEditor.resetEditorFrom(config.handler);
+        handlerStateEditor.resetEditorFrom(config.handler.getState());
       } else {
-        handlerEditor.applyEditorTo(config.handler);
-      }
-    }
-
-    private void applyTarget(BlazeCommandRunConfiguration config) {
-      String targetString = targetField.getText();
-      BlazeCommandRunConfigurationHandler oldHandler = config.handler;
-      config.setTarget(
-          Strings.isNullOrEmpty(targetString) ? null : TargetExpression.fromString(targetString));
-      if (config.handler != oldHandler) {
-        config.loadExternalElementBackup();
+        handlerStateEditor.applyEditorTo(config.handler.getState());
       }
     }
   }
diff --git a/base/src/com/google/idea/blaze/base/run/BlazeCommandRunConfigurationUpdater.java b/base/src/com/google/idea/blaze/base/run/BlazeCommandRunConfigurationUpdater.java
deleted file mode 100644
index c04c988..0000000
--- a/base/src/com/google/idea/blaze/base/run/BlazeCommandRunConfigurationUpdater.java
+++ /dev/null
@@ -1,59 +0,0 @@
-/*
- * Copyright 2016 The Bazel Authors. All rights reserved.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.google.idea.blaze.base.run;
-
-import com.google.idea.blaze.base.model.BlazeProjectData;
-import com.google.idea.blaze.base.model.primitives.Label;
-import com.google.idea.blaze.base.projectview.ProjectViewSet;
-import com.google.idea.blaze.base.run.confighandler.BlazeUnknownRunConfigurationHandler;
-import com.google.idea.blaze.base.settings.BlazeImportSettings;
-import com.google.idea.blaze.base.sync.SyncListener;
-import com.intellij.execution.RunManager;
-import com.intellij.execution.configurations.RunConfiguration;
-import com.intellij.openapi.project.Project;
-
-/**
- * Added in 1.9 to facilitate updating existing configurations to include the new handler-id and
- * kind attributes. To be removed in 2.1.
- */
-public class BlazeCommandRunConfigurationUpdater extends SyncListener.Adapter {
-  @Override
-  public void onSyncComplete(
-      Project project,
-      BlazeImportSettings importSettings,
-      ProjectViewSet projectViewSet,
-      BlazeProjectData blazeProjectData,
-      SyncResult syncResult) {
-    final RunManager runManager = RunManager.getInstance(project);
-    for (RunConfiguration configuration : runManager.getAllConfigurationsList()) {
-      if (configuration instanceof BlazeCommandRunConfiguration) {
-        BlazeCommandRunConfiguration blazeConfig = (BlazeCommandRunConfiguration) configuration;
-        // Only update configurations with unknown handlers, as this will
-        // reset any changes made to the handler data since the project loaded.
-        if (blazeConfig.getHandler() instanceof BlazeUnknownRunConfigurationHandler
-            // Also skip unresolved Label targets; these cannot safely be defaulted
-            // to the generic handler and should continue to display an error instead.
-            // If the Blaze cache is invalidated, all Labels can be unresolved;
-            // blindly updating them would result in loss of handler settings.
-            && !(blazeConfig.getTarget() instanceof Label
-                && blazeConfig.getRuleForTarget() == null)) {
-          blazeConfig.setTarget(blazeConfig.getTarget());
-          blazeConfig.loadExternalElementBackup();
-        }
-      }
-    }
-  }
-}
diff --git a/base/src/com/google/idea/blaze/base/run/BlazeRuleConfigurationFactory.java b/base/src/com/google/idea/blaze/base/run/BlazeRunConfigurationFactory.java
similarity index 70%
rename from base/src/com/google/idea/blaze/base/run/BlazeRuleConfigurationFactory.java
rename to base/src/com/google/idea/blaze/base/run/BlazeRunConfigurationFactory.java
index f430596..1010640 100644
--- a/base/src/com/google/idea/blaze/base/run/BlazeRuleConfigurationFactory.java
+++ b/base/src/com/google/idea/blaze/base/run/BlazeRunConfigurationFactory.java
@@ -15,8 +15,8 @@
  */
 package com.google.idea.blaze.base.run;
 
-import com.google.idea.blaze.base.ideinfo.RuleIdeInfo;
-import com.google.idea.blaze.base.sync.projectview.WorkspaceLanguageSettings;
+import com.google.idea.blaze.base.model.BlazeProjectData;
+import com.google.idea.blaze.base.model.primitives.Label;
 import com.intellij.execution.RunManager;
 import com.intellij.execution.RunnerAndConfigurationSettings;
 import com.intellij.execution.configurations.ConfigurationFactory;
@@ -24,14 +24,14 @@
 import com.intellij.openapi.extensions.ExtensionPointName;
 import com.intellij.openapi.project.Project;
 
-/** A factory creating run configurations based on Blaze rules. */
-public abstract class BlazeRuleConfigurationFactory {
-  public static final ExtensionPointName<BlazeRuleConfigurationFactory> EP_NAME =
-      ExtensionPointName.create("com.google.idea.blaze.RuleConfigurationFactory");
+/** A factory creating run configurations based on Blaze targets. */
+public abstract class BlazeRunConfigurationFactory {
+  public static final ExtensionPointName<BlazeRunConfigurationFactory> EP_NAME =
+      ExtensionPointName.create("com.google.idea.blaze.RunConfigurationFactory");
 
-  /** Returns whether this factory can handle a rule. */
-  public abstract boolean handlesRule(
-      WorkspaceLanguageSettings workspaceLanguageSettings, RuleIdeInfo rule);
+  /** Returns whether this factory can handle a target. */
+  public abstract boolean handlesTarget(
+      Project project, BlazeProjectData blazeProjectData, Label target);
 
   /**
    * Returns whether this factory can initialize a configuration. <br>
@@ -44,17 +44,17 @@
   }
 
   /** Constructs and initializes {@link RunnerAndConfigurationSettings} for the given rule. */
-  public RunnerAndConfigurationSettings createForRule(
-      Project project, RunManager runManager, RuleIdeInfo rule) {
+  public RunnerAndConfigurationSettings createForTarget(
+      Project project, RunManager runManager, Label target) {
     ConfigurationFactory factory = getConfigurationFactory();
     RunConfiguration configuration = factory.createTemplateConfiguration(project, runManager);
-    setupConfiguration(configuration, rule);
+    setupConfiguration(configuration, target);
     return runManager.createConfiguration(configuration, factory);
   }
 
   /** The factory used to create configurations. */
   protected abstract ConfigurationFactory getConfigurationFactory();
 
-  /** Initialize the configuration for the given rule. */
-  public abstract void setupConfiguration(RunConfiguration configuration, RuleIdeInfo rule);
+  /** Initialize the configuration for the given target. */
+  public abstract void setupConfiguration(RunConfiguration configuration, Label target);
 }
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 ea4aae6..80ae557 100755
--- a/base/src/com/google/idea/blaze/base/run/BlazeRunConfigurationSyncListener.java
+++ b/base/src/com/google/idea/blaze/base/run/BlazeRunConfigurationSyncListener.java
@@ -16,15 +16,14 @@
 package com.google.idea.blaze.base.run;
 
 import com.google.common.collect.Sets;
-import com.google.idea.blaze.base.ideinfo.RuleIdeInfo;
 import com.google.idea.blaze.base.model.BlazeProjectData;
 import com.google.idea.blaze.base.model.primitives.Label;
 import com.google.idea.blaze.base.model.primitives.TargetExpression;
 import com.google.idea.blaze.base.projectview.ProjectViewSet;
 import com.google.idea.blaze.base.projectview.section.sections.TargetSection;
+import com.google.idea.blaze.base.scope.BlazeContext;
 import com.google.idea.blaze.base.settings.BlazeImportSettings;
 import com.google.idea.blaze.base.sync.SyncListener;
-import com.google.idea.blaze.base.sync.projectview.WorkspaceLanguageSettings;
 import com.intellij.execution.RunManager;
 import com.intellij.execution.RunnerAndConfigurationSettings;
 import com.intellij.execution.configurations.RunConfiguration;
@@ -39,6 +38,7 @@
   @Override
   public void onSyncComplete(
       Project project,
+      BlazeContext context,
       BlazeImportSettings importSettings,
       ProjectViewSet projectViewSet,
       BlazeProjectData blazeProjectData,
@@ -50,13 +50,14 @@
               Set<Label> labelsWithConfigs = labelsWithConfigs(project);
               Set<TargetExpression> targetExpressions =
                   Sets.newHashSet(projectViewSet.listItems(TargetSection.KEY));
-              for (RuleIdeInfo rule : blazeProjectData.ruleMap.rules()) {
-                maybeAddRunConfiguration(
-                    project,
-                    blazeProjectData.workspaceLanguageSettings,
-                    targetExpressions,
-                    labelsWithConfigs,
-                    rule);
+              // We only auto-generate configurations for rules listed in the project view.
+              for (TargetExpression target : targetExpressions) {
+                if (!(target instanceof Label) || labelsWithConfigs.contains(target)) {
+                  continue;
+                }
+                Label label = (Label) target;
+                labelsWithConfigs.add(label);
+                maybeAddRunConfiguration(project, blazeProjectData, label);
               }
             });
   }
@@ -83,24 +84,14 @@
    * for that target.
    */
   private static void maybeAddRunConfiguration(
-      Project project,
-      WorkspaceLanguageSettings workspaceLanguageSettings,
-      Set<TargetExpression> importTargets,
-      Set<Label> labelsWithConfigs,
-      RuleIdeInfo rule) {
-    Label label = rule.label;
-    // We only auto-generate configurations for rules listed in the project view.
-    if (!importTargets.contains(label) || labelsWithConfigs.contains(label)) {
-      return;
-    }
-    labelsWithConfigs.add(label);
+      Project project, BlazeProjectData blazeProjectData, Label label) {
     final RunManager runManager = RunManager.getInstance(project);
 
-    for (BlazeRuleConfigurationFactory configurationFactory :
-        BlazeRuleConfigurationFactory.EP_NAME.getExtensions()) {
-      if (configurationFactory.handlesRule(workspaceLanguageSettings, rule)) {
+    for (BlazeRunConfigurationFactory configurationFactory :
+        BlazeRunConfigurationFactory.EP_NAME.getExtensions()) {
+      if (configurationFactory.handlesTarget(project, blazeProjectData, label)) {
         final RunnerAndConfigurationSettings settings =
-            configurationFactory.createForRule(project, runManager, rule);
+            configurationFactory.createForTarget(project, runManager, label);
         runManager.addConfiguration(settings, false /* isShared */);
         if (runManager.getSelectedConfiguration() == null) {
           // TODO(joshgiles): Better strategy for picking initially selected config.
diff --git a/base/src/com/google/idea/blaze/base/run/RuleNameHeuristic.java b/base/src/com/google/idea/blaze/base/run/RuleNameHeuristic.java
index 8f92d75..f766ceb 100644
--- a/base/src/com/google/idea/blaze/base/run/RuleNameHeuristic.java
+++ b/base/src/com/google/idea/blaze/base/run/RuleNameHeuristic.java
@@ -26,7 +26,16 @@
 
   @Override
   public boolean matchesSource(RuleIdeInfo rule, File sourceFile, @Nullable TestSize testSize) {
-    String sourceName = FileUtil.getNameWithoutExtension(sourceFile);
-    return sourceName.equals(rule.label.ruleName().toString());
+    String filePathWithoutExtension = FileUtil.getNameWithoutExtension(sourceFile.getPath());
+    String ruleName = rule.label.ruleName().toString();
+    if (!filePathWithoutExtension.endsWith(ruleName)) {
+      return false;
+    }
+    int i = filePathWithoutExtension.length() - ruleName.length() - 1;
+    if (i < 0) {
+      // Equal length
+      return true;
+    }
+    return filePathWithoutExtension.charAt(i) == '/';
   }
 }
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 97a9aaa..0080b3f 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
@@ -15,226 +15,52 @@
  */
 package com.google.idea.blaze.base.run.confighandler;
 
-import com.google.common.base.Strings;
-import com.google.common.collect.ImmutableList;
-import com.google.idea.blaze.base.async.process.LineProcessingOutputStream;
-import com.google.idea.blaze.base.command.BlazeCommand;
 import com.google.idea.blaze.base.command.BlazeCommandName;
-import com.google.idea.blaze.base.command.BlazeFlags;
-import com.google.idea.blaze.base.issueparser.IssueOutputLineProcessor;
-import com.google.idea.blaze.base.metrics.Action;
-import com.google.idea.blaze.base.model.primitives.WorkspaceRoot;
-import com.google.idea.blaze.base.projectview.ProjectViewManager;
-import com.google.idea.blaze.base.projectview.ProjectViewSet;
 import com.google.idea.blaze.base.run.BlazeCommandRunConfiguration;
 import com.google.idea.blaze.base.run.BlazeConfigurationNameBuilder;
-import com.google.idea.blaze.base.run.processhandler.LineProcessingProcessAdapter;
-import com.google.idea.blaze.base.run.processhandler.ScopedBlazeProcessHandler;
-import com.google.idea.blaze.base.scope.BlazeContext;
-import com.google.idea.blaze.base.scope.scopes.IdeaLogScope;
-import com.google.idea.blaze.base.scope.scopes.IssuesScope;
-import com.google.idea.blaze.base.scope.scopes.LoggedTimingScope;
+import com.google.idea.blaze.base.run.state.BlazeCommandRunConfigurationCommonState;
 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.ui.UiUtil;
-import com.intellij.execution.ExecutionException;
 import com.intellij.execution.Executor;
-import com.intellij.execution.configurations.CommandLineState;
 import com.intellij.execution.configurations.RunConfiguration;
-import com.intellij.execution.configurations.RunProfile;
-import com.intellij.execution.configurations.RunProfileState;
-import com.intellij.execution.configurations.RuntimeConfigurationError;
 import com.intellij.execution.configurations.RuntimeConfigurationException;
-import com.intellij.execution.process.ProcessHandler;
-import com.intellij.execution.process.ProcessListener;
 import com.intellij.execution.runners.ExecutionEnvironment;
-import com.intellij.openapi.project.Project;
-import com.intellij.openapi.ui.ComboBox;
-import com.intellij.openapi.util.InvalidDataException;
-import com.intellij.ui.components.JBTextField;
-import com.intellij.util.execution.ParametersListUtil;
-import java.io.File;
-import java.util.List;
 import javax.annotation.Nullable;
-import javax.swing.DefaultComboBoxModel;
 import javax.swing.Icon;
-import javax.swing.JComponent;
-import javax.swing.JLabel;
-import javax.swing.JScrollPane;
-import javax.swing.JTextArea;
-import javax.swing.ScrollPaneConstants;
-import org.jdom.Element;
-import org.jetbrains.annotations.NotNull;
 
 /**
  * Generic handler for {@link BlazeCommandRunConfiguration}s, used as a fallback in the case where
  * no other handlers are more relevant.
  */
-public class BlazeCommandGenericRunConfigurationHandler
+public final class BlazeCommandGenericRunConfigurationHandler
     implements BlazeCommandRunConfigurationHandler {
-  private static final String COMMAND_ATTR = "blaze-command";
-  private static final String USER_BLAZE_FLAG_TAG = "blaze-user-flag";
-  private static final String USER_EXE_FLAG_TAG = "blaze-user-exe-flag";
-  private static final String BLAZE_BINARY_TAG = "blaze-binary";
 
-  /** The configuration this handler is for. */
-  protected final BlazeCommandRunConfiguration configuration;
-
-  @Nullable private BlazeCommandName command;
-  @Nullable private String blazeBinary;
-  private ImmutableList<String> blazeFlags = ImmutableList.of();
-  private ImmutableList<String> exeFlags = ImmutableList.of();
+  private final String buildSystemName;
+  private final BlazeCommandRunConfigurationCommonState state;
 
   public BlazeCommandGenericRunConfigurationHandler(BlazeCommandRunConfiguration configuration) {
-    this.configuration = configuration;
+    this.buildSystemName = Blaze.buildSystemName(configuration.getProject());
+    this.state = new BlazeCommandRunConfigurationCommonState(buildSystemName);
   }
 
-  protected BlazeCommandGenericRunConfigurationHandler(
-      BlazeCommandGenericRunConfigurationHandler other,
-      BlazeCommandRunConfiguration configuration) {
-    this(configuration);
-    command = other.command;
-    blazeFlags = other.blazeFlags;
-    exeFlags = other.exeFlags;
-    blazeBinary = other.blazeBinary;
+  @Override
+  public BlazeCommandRunConfigurationCommonState getState() {
+    return state;
   }
 
-  @Nullable
-  public BlazeCommandName getCommand() {
-    return command;
-  }
-
-  /** @return The list of blaze flags that the user specified manually. */
-  public List<String> getBlazeFlags() {
-    return blazeFlags;
-  }
-
-  /** @return The list of executable flags the user specified manually. */
-  public List<String> getExeFlags() {
-    return exeFlags;
-  }
-
-  /**
-   * @return The list of all flags to be used on the Blaze command line for blaze. Subclasses should
-   *     override this method to add flags for higher-level settings (e.g. "run locally").
-   */
-  public List<String> getAllBlazeFlags() {
-    return getBlazeFlags();
-  }
-
-  @Nullable
-  public String getBlazeBinary() {
-    return blazeBinary;
-  }
-
-  /**
-   * @return The list of all flags to be used for the executable on the Blaze command line.
-   *     Subclasses should override this method to add flags if desired.
-   */
-  public List<String> getAllExeFlags() {
-    return getExeFlags();
-  }
-
-  public void setCommand(@Nullable BlazeCommandName command) {
-    this.command = command;
-  }
-
-  public final void setBlazeFlags(List<String> flags) {
-    this.blazeFlags = ImmutableList.copyOf(flags);
-  }
-
-  public final void setExeFlags(List<String> flags) {
-    this.exeFlags = ImmutableList.copyOf(flags);
-  }
-
-  public void setBlazeBinary(@Nullable String blazeBinary) {
-    this.blazeBinary = blazeBinary;
-  }
-
-  /** Searches through all blaze flags for the first one beginning with '--test_filter' */
-  @Nullable
-  public String getTestFilterFlag() {
-    for (String flag : getAllBlazeFlags()) {
-      if (flag.startsWith(BlazeFlags.TEST_FILTER)) {
-        return flag;
-      }
-    }
-    return null;
+  @Override
+  public BlazeCommandRunConfigurationRunner createRunner(
+      Executor executor, ExecutionEnvironment environment) {
+    return new BlazeCommandGenericRunConfigurationRunner();
   }
 
   @Override
   public void checkConfiguration() throws RuntimeConfigurationException {
-    if (command == null) {
-      throw new RuntimeConfigurationError("You must specify a command.");
-    }
-    if (blazeBinary != null && !(new File(blazeBinary).exists())) {
-      throw new RuntimeConfigurationError(
-          Blaze.buildSystemName(configuration.getProject()) + " binary does not exist");
-    }
-  }
-
-  @Override
-  public void readExternal(Element element) throws InvalidDataException {
-    String commandString = element.getAttributeValue(COMMAND_ATTR);
-    command =
-        Strings.isNullOrEmpty(commandString) ? null : BlazeCommandName.fromString(commandString);
-    blazeFlags = loadUserFlags(element, USER_BLAZE_FLAG_TAG);
-    exeFlags = loadUserFlags(element, USER_EXE_FLAG_TAG);
-    blazeBinary = element.getAttributeValue(BLAZE_BINARY_TAG);
-  }
-
-  private static ImmutableList<String> loadUserFlags(Element root, String tag) {
-    ImmutableList.Builder<String> flagsBuilder = ImmutableList.builder();
-    for (Element e : root.getChildren(tag)) {
-      String flag = e.getTextTrim();
-      if (flag != null && !flag.isEmpty()) {
-        flagsBuilder.add(flag);
-      }
-    }
-    return flagsBuilder.build();
-  }
-
-  @Override
-  public void writeExternal(Element element) {
-    if (command != null) {
-      element.setAttribute(COMMAND_ATTR, command.toString());
-    }
-    saveUserFlags(element, blazeFlags, USER_BLAZE_FLAG_TAG);
-    saveUserFlags(element, exeFlags, USER_EXE_FLAG_TAG);
-    if (!Strings.isNullOrEmpty(blazeBinary)) {
-      element.setAttribute(BLAZE_BINARY_TAG, blazeBinary);
-    }
-  }
-
-  private static void saveUserFlags(Element root, List<String> flags, String tag) {
-    for (String flag : flags) {
-      Element child = new Element(tag);
-      child.setText(flag);
-      root.addContent(child);
-    }
-  }
-
-  @Override
-  public BlazeCommandGenericRunConfigurationHandler cloneFor(
-      BlazeCommandRunConfiguration configuration) {
-    return new BlazeCommandGenericRunConfigurationHandler(this, configuration);
-  }
-
-  @Override
-  public RunProfileState getState(Executor executor, ExecutionEnvironment environment) {
-    return new BlazeCommandGenericRunConfigurationHandler.BlazeCommandRunProfileState(environment);
-  }
-
-  @Override
-  public boolean executeBeforeRunTask(ExecutionEnvironment environment) {
-    // Don't execute any tasks.
-    return true;
+    state.validate(buildSystemName);
   }
 
   @Override
   @Nullable
-  public String suggestedName() {
+  public String suggestedName(BlazeCommandRunConfiguration configuration) {
     if (configuration.getTarget() == null) {
       return null;
     }
@@ -244,15 +70,11 @@
   @Override
   @Nullable
   public String getCommandName() {
+    BlazeCommandName command = state.getCommand();
     return command != null ? command.toString() : null;
   }
 
   @Override
-  public boolean isGeneratedName(boolean hasGeneratedFlag) {
-    return hasGeneratedFlag;
-  }
-
-  @Override
   public String getHandlerName() {
     return "Generic Handler";
   }
@@ -262,163 +84,4 @@
   public Icon getExecutorIcon(RunConfiguration configuration, Executor executor) {
     return null;
   }
-
-  @Override
-  public BlazeCommandRunConfigurationHandlerEditor getHandlerEditor() {
-    return new BlazeCommandGenericRunConfigurationHandler
-        .BlazeCommandGenericRunConfigurationHandlerEditor(this);
-  }
-
-  /** {@link RunProfileState} for generic blaze commands. */
-  private static class BlazeCommandRunProfileState extends CommandLineState {
-    private final BlazeCommandRunConfiguration configuration;
-    private final BlazeCommandGenericRunConfigurationHandler handler;
-
-    BlazeCommandRunProfileState(ExecutionEnvironment environment) {
-      super(environment);
-      RunProfile runProfile = environment.getRunProfile();
-      configuration = (BlazeCommandRunConfiguration) runProfile;
-      handler = (BlazeCommandGenericRunConfigurationHandler) configuration.getHandler();
-    }
-
-    @Override
-    @NotNull
-    protected ProcessHandler startProcess() throws ExecutionException {
-      Project project = configuration.getProject();
-      BlazeImportSettings importSettings =
-          BlazeImportSettingsManager.getInstance(project).getImportSettings();
-      assert importSettings != null;
-
-      ProjectViewSet projectViewSet = ProjectViewManager.getInstance(project).getProjectViewSet();
-      assert projectViewSet != null;
-
-      BlazeCommand blazeCommand =
-          BlazeCommand.builder(Blaze.getBuildSystem(project), handler.getCommand())
-              .setBlazeBinary(handler.getBlazeBinary())
-              .addTargets(configuration.getTarget())
-              .addBlazeFlags(BlazeFlags.buildFlags(project, projectViewSet))
-              .addBlazeFlags(handler.getAllBlazeFlags())
-              .addExeFlags(handler.getAllExeFlags())
-              .build();
-
-      WorkspaceRoot workspaceRoot = WorkspaceRoot.fromImportSettings(importSettings);
-      return new ScopedBlazeProcessHandler(
-          project,
-          blazeCommand,
-          workspaceRoot,
-          new ScopedBlazeProcessHandler.ScopedProcessHandlerDelegate() {
-            @Override
-            public void onBlazeContextStart(BlazeContext context) {
-              context
-                  .push(new LoggedTimingScope(project, Action.BLAZE_COMMAND_USAGE))
-                  .push(new IssuesScope(project))
-                  .push(new IdeaLogScope());
-            }
-
-            @Override
-            public ImmutableList<ProcessListener> createProcessListeners(BlazeContext context) {
-              LineProcessingOutputStream outputStream =
-                  LineProcessingOutputStream.of(
-                      new IssueOutputLineProcessor(project, context, workspaceRoot));
-              return ImmutableList.of(new LineProcessingProcessAdapter(outputStream));
-            }
-          });
-    }
-  }
-
-  /** {@link BlazeCommandRunConfigurationHandlerEditor} for generic blaze commands. */
-  static class BlazeCommandGenericRunConfigurationHandlerEditor
-      implements BlazeCommandRunConfigurationHandlerEditor {
-    private final String buildSystemName;
-
-    private final ComboBox commandCombo;
-    private final JTextArea blazeFlagsField = new JTextArea(5, 1);
-    private final JTextArea exeFlagsField = new JTextArea(5, 1);
-    private final JBTextField blazeBinaryField = new JBTextField(1);
-
-    public BlazeCommandGenericRunConfigurationHandlerEditor(
-        BlazeCommandGenericRunConfigurationHandler handler) {
-      buildSystemName = Blaze.buildSystemName(handler.configuration.getProject());
-      commandCombo =
-          new ComboBox(new DefaultComboBoxModel(BlazeCommandName.knownCommands().toArray()));
-      // Allow the user to manually specify an unlisted command.
-      commandCombo.setEditable(true);
-      blazeBinaryField.getEmptyText().setText("(Use global)");
-    }
-
-    private static String makeArgString(List<String> arguments) {
-      StringBuilder flagString = new StringBuilder();
-      for (String flag : arguments) {
-        if (flagString.length() > 0) {
-          flagString.append('\n');
-        }
-        if (flag.isEmpty() || flag.contains(" ") || flag.contains("|")) {
-          flagString.append('"');
-          flagString.append(flag);
-          flagString.append('"');
-        } else {
-          flagString.append(flag);
-        }
-      }
-      return flagString.toString();
-    }
-
-    @Override
-    public void resetEditorFrom(BlazeCommandRunConfigurationHandler h) {
-      BlazeCommandGenericRunConfigurationHandler handler =
-          (BlazeCommandGenericRunConfigurationHandler) h;
-
-      commandCombo.setSelectedItem(handler.command);
-
-      // Normally we could just use ParametersListUtils.join, but that will only space-delimit args
-      blazeFlagsField.setText(makeArgString(handler.getBlazeFlags()));
-      exeFlagsField.setText(makeArgString(handler.getExeFlags()));
-
-      blazeBinaryField.setText(Strings.nullToEmpty(handler.blazeBinary));
-    }
-
-    @Override
-    public void applyEditorTo(BlazeCommandRunConfigurationHandler h) {
-      BlazeCommandGenericRunConfigurationHandler handler =
-          (BlazeCommandGenericRunConfigurationHandler) h;
-      Object selectedCommand = commandCombo.getSelectedItem();
-      if (selectedCommand instanceof BlazeCommandName) {
-        handler.command = (BlazeCommandName) selectedCommand;
-      } else {
-        handler.command =
-            Strings.isNullOrEmpty((String) selectedCommand)
-                ? null
-                : BlazeCommandName.fromString(selectedCommand.toString());
-      }
-      handler.blazeFlags =
-          ImmutableList.copyOf(
-              ParametersListUtil.parse(Strings.nullToEmpty(blazeFlagsField.getText())));
-      handler.exeFlags =
-          ImmutableList.copyOf(
-              ParametersListUtil.parse(Strings.nullToEmpty(exeFlagsField.getText())));
-
-      String blazeBinary = blazeBinaryField.getText();
-      handler.blazeBinary = Strings.emptyToNull(blazeBinary);
-    }
-
-    @Override
-    @NotNull
-    public JComponent createEditor() {
-      return UiUtil.createBox(
-          new JLabel(buildSystemName + " command:"),
-          commandCombo,
-          new JLabel(buildSystemName + " flags:"),
-          new JScrollPane(
-              blazeFlagsField,
-              JScrollPane.VERTICAL_SCROLLBAR_AS_NEEDED,
-              ScrollPaneConstants.HORIZONTAL_SCROLLBAR_AS_NEEDED),
-          new JLabel("Executable flags:"),
-          new JScrollPane(
-              exeFlagsField,
-              JScrollPane.VERTICAL_SCROLLBAR_AS_NEEDED,
-              ScrollPaneConstants.HORIZONTAL_SCROLLBAR_AS_NEEDED),
-          new JLabel(buildSystemName + " binary:"),
-          blazeBinaryField);
-    }
-  }
 }
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
new file mode 100644
index 0000000..ed40a0d
--- /dev/null
+++ b/base/src/com/google/idea/blaze/base/run/confighandler/BlazeCommandGenericRunConfigurationRunner.java
@@ -0,0 +1,124 @@
+/*
+ * Copyright 2016 The Bazel Authors. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.idea.blaze.base.run.confighandler;
+
+import com.google.common.collect.ImmutableList;
+import com.google.idea.blaze.base.async.process.LineProcessingOutputStream;
+import com.google.idea.blaze.base.command.BlazeCommand;
+import com.google.idea.blaze.base.command.BlazeFlags;
+import com.google.idea.blaze.base.issueparser.IssueOutputLineProcessor;
+import com.google.idea.blaze.base.metrics.Action;
+import com.google.idea.blaze.base.model.primitives.WorkspaceRoot;
+import com.google.idea.blaze.base.projectview.ProjectViewManager;
+import com.google.idea.blaze.base.projectview.ProjectViewSet;
+import com.google.idea.blaze.base.run.BlazeCommandRunConfiguration;
+import com.google.idea.blaze.base.run.processhandler.LineProcessingProcessAdapter;
+import com.google.idea.blaze.base.run.processhandler.ScopedBlazeProcessHandler;
+import com.google.idea.blaze.base.run.state.BlazeCommandRunConfigurationCommonState;
+import com.google.idea.blaze.base.scope.BlazeContext;
+import com.google.idea.blaze.base.scope.scopes.IdeaLogScope;
+import com.google.idea.blaze.base.scope.scopes.IssuesScope;
+import com.google.idea.blaze.base.scope.scopes.LoggedTimingScope;
+import com.google.idea.blaze.base.settings.Blaze;
+import com.google.idea.blaze.base.settings.BlazeImportSettings;
+import com.google.idea.blaze.base.settings.BlazeImportSettingsManager;
+import com.intellij.execution.ExecutionException;
+import com.intellij.execution.Executor;
+import com.intellij.execution.configurations.CommandLineState;
+import com.intellij.execution.configurations.RunProfile;
+import com.intellij.execution.configurations.RunProfileState;
+import com.intellij.execution.process.ProcessHandler;
+import com.intellij.execution.process.ProcessListener;
+import com.intellij.execution.runners.ExecutionEnvironment;
+import com.intellij.openapi.project.Project;
+import org.jetbrains.annotations.NotNull;
+
+/**
+ * Generic runner for {@link BlazeCommandRunConfiguration}s, used as a fallback in the case where no
+ * other runners are more relevant.
+ */
+public final class BlazeCommandGenericRunConfigurationRunner
+    implements BlazeCommandRunConfigurationRunner {
+
+  @Override
+  public RunProfileState getRunProfileState(Executor executor, ExecutionEnvironment environment) {
+    return new BlazeCommandRunProfileState(environment);
+  }
+
+  @Override
+  public boolean executeBeforeRunTask(ExecutionEnvironment environment) {
+    // Don't execute any tasks.
+    return true;
+  }
+
+  /** {@link RunProfileState} for generic blaze commands. */
+  private static class BlazeCommandRunProfileState extends CommandLineState {
+    private final BlazeCommandRunConfiguration configuration;
+    private final BlazeCommandRunConfigurationCommonState handlerState;
+
+    BlazeCommandRunProfileState(ExecutionEnvironment environment) {
+      super(environment);
+      RunProfile runProfile = environment.getRunProfile();
+      configuration = (BlazeCommandRunConfiguration) runProfile;
+      handlerState =
+          (BlazeCommandRunConfigurationCommonState) configuration.getHandler().getState();
+    }
+
+    @Override
+    @NotNull
+    protected ProcessHandler startProcess() throws ExecutionException {
+      Project project = configuration.getProject();
+      BlazeImportSettings importSettings =
+          BlazeImportSettingsManager.getInstance(project).getImportSettings();
+      assert importSettings != null;
+
+      ProjectViewSet projectViewSet = ProjectViewManager.getInstance(project).getProjectViewSet();
+      assert projectViewSet != null;
+
+      BlazeCommand blazeCommand =
+          BlazeCommand.builder(Blaze.getBuildSystem(project), handlerState.getCommand())
+              .setBlazeBinary(handlerState.getBlazeBinary())
+              .addTargets(configuration.getTarget())
+              .addBlazeFlags(BlazeFlags.buildFlags(project, projectViewSet))
+              .addBlazeFlags(handlerState.getBlazeFlags())
+              .addExeFlags(handlerState.getExeFlags())
+              .build();
+
+      WorkspaceRoot workspaceRoot = WorkspaceRoot.fromImportSettings(importSettings);
+      return new ScopedBlazeProcessHandler(
+          project,
+          blazeCommand,
+          workspaceRoot,
+          new ScopedBlazeProcessHandler.ScopedProcessHandlerDelegate() {
+            @Override
+            public void onBlazeContextStart(BlazeContext context) {
+              context
+                  .push(new LoggedTimingScope(project, Action.BLAZE_COMMAND_USAGE))
+                  .push(new IssuesScope(project))
+                  .push(new IdeaLogScope());
+            }
+
+            @Override
+            public ImmutableList<ProcessListener> createProcessListeners(BlazeContext context) {
+              LineProcessingOutputStream outputStream =
+                  LineProcessingOutputStream.of(
+                      new IssueOutputLineProcessor(project, context, workspaceRoot));
+              return ImmutableList.of(new LineProcessingProcessAdapter(outputStream));
+            }
+          });
+    }
+  }
+}
diff --git a/base/src/com/google/idea/blaze/base/run/confighandler/BlazeCommandRunConfigurationHandler.java b/base/src/com/google/idea/blaze/base/run/confighandler/BlazeCommandRunConfigurationHandler.java
index 7d88a7e..0e21967 100644
--- a/base/src/com/google/idea/blaze/base/run/confighandler/BlazeCommandRunConfigurationHandler.java
+++ b/base/src/com/google/idea/blaze/base/run/confighandler/BlazeCommandRunConfigurationHandler.java
@@ -16,25 +16,29 @@
 package com.google.idea.blaze.base.run.confighandler;
 
 import com.google.idea.blaze.base.run.BlazeCommandRunConfiguration;
+import com.google.idea.blaze.base.run.state.RunConfigurationState;
 import com.intellij.execution.ExecutionException;
 import com.intellij.execution.Executor;
 import com.intellij.execution.configurations.RunConfiguration;
-import com.intellij.execution.configurations.RunProfileState;
 import com.intellij.execution.configurations.RuntimeConfigurationException;
 import com.intellij.execution.runners.ExecutionEnvironment;
-import com.intellij.openapi.util.InvalidDataException;
-import com.intellij.openapi.util.WriteExternalException;
 import javax.annotation.Nullable;
 import javax.swing.Icon;
-import org.jdom.Element;
 
 /**
  * Supports the run configuration flow for {@link BlazeCommandRunConfiguration}s.
  *
- * <p>Provides rule-specific configuration state, editor, name, RunProfileState, and
- * before-run-tasks.
+ * <p>Provides rule-specific configuration state, validation, presentation, and runner.
  */
 public interface BlazeCommandRunConfigurationHandler {
+  RunConfigurationState getState();
+
+
+
+  /** @return A {@link BlazeCommandRunConfigurationRunner} for running the configuration. */
+  @Nullable
+  BlazeCommandRunConfigurationRunner createRunner(
+      Executor executor, ExecutionEnvironment environment) throws ExecutionException;
 
   /**
    * Checks whether the handler settings are valid.
@@ -43,50 +47,12 @@
    */
   void checkConfiguration() throws RuntimeConfigurationException;
 
-  /** Loads this handler's state from the external data. */
-  void readExternal(Element element) throws InvalidDataException;
-
-  /** Writes this handler's state to the element. */
-  @SuppressWarnings("ThrowsUncheckedException")
-  void writeExternal(Element element) throws WriteExternalException;
-
-  /**
-   * Creates a clone of this handler for the specified configuration.
-   *
-   * @return A new BlazeCommandRunConfigurationHandler with the same state as this one, except its
-   *     configuration is the specified {@code configuration}.
-   */
-  BlazeCommandRunConfigurationHandler cloneFor(BlazeCommandRunConfiguration configuration);
-
-  /** @return the RunProfileState corresponding to the given environment. */
-  RunProfileState getState(Executor executor, ExecutionEnvironment environment)
-      throws ExecutionException;
-
-  /**
-   * Executes any required before run tasks.
-   *
-   * @return true if no task exists or the task was successfully completed. Otherwise returns false
-   *     if the task either failed or was cancelled.
-   */
-  boolean executeBeforeRunTask(ExecutionEnvironment environment);
-
   /**
    * @return The default name of the run configuration based on its settings and this handler's
    *     state.
    */
   @Nullable
-  String suggestedName();
-
-  /**
-   * Allows overriding the default behavior of {@link
-   * com.intellij.execution.configurations.LocatableConfiguration#isGeneratedName()}. Return {@code
-   * hasGeneratedFlag} to keep the default behavior.
-   *
-   * @param hasGeneratedFlag Whether the configuration reports its name is generated.
-   * @return Whether the run configuration's name should be treated as generated (allowing
-   *     regenerating it when settings change).
-   */
-  boolean isGeneratedName(boolean hasGeneratedFlag);
+  String suggestedName(BlazeCommandRunConfiguration configuration);
 
   /**
    * @return The name of the Blaze command associated with this handler. May be null if no command
@@ -105,7 +71,4 @@
    */
   @Nullable
   Icon getExecutorIcon(RunConfiguration configuration, Executor executor);
-
-  /** @return A {@link BlazeCommandRunConfigurationHandlerEditor} for this handler. */
-  BlazeCommandRunConfigurationHandlerEditor getHandlerEditor();
 }
diff --git a/base/src/com/google/idea/blaze/base/run/confighandler/BlazeCommandRunConfigurationHandlerEditor.java b/base/src/com/google/idea/blaze/base/run/confighandler/BlazeCommandRunConfigurationHandlerEditor.java
deleted file mode 100644
index 0c9bce7..0000000
--- a/base/src/com/google/idea/blaze/base/run/confighandler/BlazeCommandRunConfigurationHandlerEditor.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.base.run.confighandler;
-
-import javax.swing.JComponent;
-import org.jetbrains.annotations.Nullable;
-
-/** Provides support for editing {@link BlazeCommandRunConfigurationHandler}s. */
-public interface BlazeCommandRunConfigurationHandlerEditor {
-  /** Reset the editor based on the state of the given handler. */
-  void resetEditorFrom(BlazeCommandRunConfigurationHandler handler);
-
-  /** Update the given handler's state based on the editor. */
-  void applyEditorTo(BlazeCommandRunConfigurationHandler handler);
-
-  /** @return A component to display for the editor. */
-  @Nullable
-  JComponent createEditor();
-}
diff --git a/base/src/com/google/idea/blaze/base/run/confighandler/BlazeCommandRunConfigurationHandlerProvider.java b/base/src/com/google/idea/blaze/base/run/confighandler/BlazeCommandRunConfigurationHandlerProvider.java
index af67362..648f2c3 100644
--- a/base/src/com/google/idea/blaze/base/run/confighandler/BlazeCommandRunConfigurationHandlerProvider.java
+++ b/base/src/com/google/idea/blaze/base/run/confighandler/BlazeCommandRunConfigurationHandlerProvider.java
@@ -40,10 +40,8 @@
         return handlerProvider;
       }
     }
-    // BlazeCommandGenericRunConfigurationHandlerProvider can handle any kind,
-    // and will be returned by this point.
-    assert false;
-    return null;
+    throw new RuntimeException(
+        "No BlazeCommandRunConfigurationHandlerProvider found for Kind " + kind);
   }
 
   /** Get the BlazeCommandRunConfigurationHandlerProvider with the given ID, if one exists. */
@@ -61,7 +59,7 @@
   boolean canHandleKind(Kind kind);
 
   /** Returns the corresponding {@link BlazeCommandRunConfigurationHandler}. */
-  BlazeCommandRunConfigurationHandler createHandler(BlazeCommandRunConfiguration config);
+  BlazeCommandRunConfigurationHandler createHandler(BlazeCommandRunConfiguration configuration);
 
   /**
    * Returns the unique ID of this {@link BlazeCommandRunConfigurationHandlerProvider}. The ID is
diff --git a/base/src/com/google/idea/blaze/base/run/confighandler/BlazeCommandRunConfigurationRunner.java b/base/src/com/google/idea/blaze/base/run/confighandler/BlazeCommandRunConfigurationRunner.java
new file mode 100644
index 0000000..bf0dd6f
--- /dev/null
+++ b/base/src/com/google/idea/blaze/base/run/confighandler/BlazeCommandRunConfigurationRunner.java
@@ -0,0 +1,45 @@
+/*
+ * Copyright 2016 The Bazel Authors. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.idea.blaze.base.run.confighandler;
+
+import com.google.idea.blaze.base.run.BlazeCommandRunConfiguration;
+import com.intellij.execution.ExecutionException;
+import com.intellij.execution.Executor;
+import com.intellij.execution.configurations.RunProfileState;
+import com.intellij.execution.runners.ExecutionEnvironment;
+import com.intellij.openapi.util.Key;
+
+/**
+ * Supports the execution of {@link BlazeCommandRunConfiguration}s.
+ *
+ * <p>Provides rule-specific RunProfileState and before-run-tasks.
+ */
+public interface BlazeCommandRunConfigurationRunner {
+  /** Used to store a runner to an {@link ExecutionEnvironment}. */
+  Key<BlazeCommandRunConfigurationRunner> RUNNER_KEY = Key.create("blaze.run.config.runner");
+
+  /** @return the RunProfileState corresponding to the given environment. */
+  RunProfileState getRunProfileState(Executor executor, ExecutionEnvironment environment)
+      throws ExecutionException;
+
+  /**
+   * Executes any required before run tasks.
+   *
+   * @return true if no task exists or the task was successfully completed. Otherwise returns false
+   *     if the task either failed or was cancelled.
+   */
+  boolean executeBeforeRunTask(ExecutionEnvironment environment);
+}
diff --git a/base/src/com/google/idea/blaze/base/run/confighandler/BlazeUnknownRunConfigurationHandler.java b/base/src/com/google/idea/blaze/base/run/confighandler/BlazeUnknownRunConfigurationHandler.java
deleted file mode 100644
index e968860..0000000
--- a/base/src/com/google/idea/blaze/base/run/confighandler/BlazeUnknownRunConfigurationHandler.java
+++ /dev/null
@@ -1,158 +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.run.confighandler;
-
-import com.google.idea.blaze.base.run.BlazeCommandRunConfiguration;
-import com.google.idea.blaze.base.settings.Blaze;
-import com.intellij.execution.ExecutionException;
-import com.intellij.execution.Executor;
-import com.intellij.execution.configurations.RunConfiguration;
-import com.intellij.execution.configurations.RunProfileState;
-import com.intellij.execution.configurations.RuntimeConfigurationException;
-import com.intellij.execution.runners.ExecutionEnvironment;
-import com.intellij.openapi.util.InvalidDataException;
-import java.util.HashSet;
-import java.util.Set;
-import javax.annotation.Nullable;
-import javax.swing.Icon;
-import javax.swing.JComponent;
-import org.jdom.Attribute;
-import org.jdom.Element;
-
-/**
- * Fallback handler for {@link BlazeCommandRunConfiguration}s with uninitialized targets or unknown
- * handler providers.
- *
- * <p>Cannot be run and provides no editor. Writes all attributes and elements it initially read,
- * except those with names matching existing content written by the configuration itself.
- */
-public class BlazeUnknownRunConfigurationHandler implements BlazeCommandRunConfigurationHandler {
-
-  private final BlazeCommandRunConfiguration configuration;
-
-  @Nullable private Element externalElementBackup;
-
-  public BlazeUnknownRunConfigurationHandler(BlazeCommandRunConfiguration configuration) {
-    this.configuration = configuration;
-  }
-
-  @Override
-  public void checkConfiguration() throws RuntimeConfigurationException {
-    // No need to throw anything here; BlazeCommandRunConfiguration's
-    // check will already detect any config with this handler as invalid
-    // because its provider is null and all targets are handled by some provider.
-    assert false;
-  }
-
-  @Override
-  public void readExternal(Element element) throws InvalidDataException {
-    externalElementBackup = element.clone();
-  }
-
-  @Override
-  public void writeExternal(Element element) {
-    // Write back attributes and elements from externalElementBackup,
-    // but take care not to write any which exist in the passed element.
-    // Such attributes and elements belong to the configuration, not the handler!
-    if (externalElementBackup != null) {
-      Set<String> configurationAttributeNames = new HashSet<>();
-      for (Attribute attribute : element.getAttributes()) {
-        configurationAttributeNames.add(attribute.getName());
-      }
-      Set<String> configurationElementNames = new HashSet<>();
-      for (Element child : element.getChildren()) {
-        configurationElementNames.add(child.getName());
-      }
-
-      for (Attribute attribute : externalElementBackup.getAttributes()) {
-        if (!configurationAttributeNames.contains(attribute.getName())) {
-          element.setAttribute(attribute.clone());
-        }
-      }
-      for (Element child : externalElementBackup.getChildren()) {
-        if (!configurationElementNames.contains(child.getName())) {
-          element.addContent(child.clone());
-        }
-      }
-    }
-  }
-
-  @Override
-  public BlazeUnknownRunConfigurationHandler cloneFor(BlazeCommandRunConfiguration configuration) {
-    return new BlazeUnknownRunConfigurationHandler(configuration);
-  }
-
-  @Override
-  public RunProfileState getState(Executor executor, ExecutionEnvironment environment)
-      throws ExecutionException {
-    return null;
-  }
-
-  @Override
-  public boolean executeBeforeRunTask(ExecutionEnvironment environment) {
-    return true;
-  }
-
-  @Nullable
-  @Override
-  public String suggestedName() {
-    return String.format(
-        "Unknown %s Configuration", Blaze.buildSystemName(configuration.getProject()));
-  }
-
-  @Override
-  public boolean isGeneratedName(boolean hasGeneratedFlag) {
-    return hasGeneratedFlag;
-  }
-
-  @Nullable
-  @Override
-  public String getCommandName() {
-    return null;
-  }
-
-  @Override
-  public String getHandlerName() {
-    return "no handler";
-  }
-
-  @Override
-  @Nullable
-  public Icon getExecutorIcon(RunConfiguration configuration, Executor executor) {
-    return null;
-  }
-
-  @Override
-  public BlazeCommandRunConfigurationHandlerEditor getHandlerEditor() {
-    return new BlazeCommandRunConfigurationHandlerEditor() {
-      @Override
-      public void resetEditorFrom(BlazeCommandRunConfigurationHandler handler) {}
-
-      @Override
-      public void applyEditorTo(BlazeCommandRunConfigurationHandler handler) {}
-
-      @Override
-      @Nullable
-      public JComponent createEditor() {
-        // Note: currently this will never be displayed,
-        // as the handler editor is not shown for invalidated configurations.
-        //return new JBLabel("Configuration could not be loaded "
-        //    + "because its handler could not be found.");
-        return null;
-      }
-    };
-  }
-}
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 e3746f3..154a9be 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
@@ -21,7 +21,7 @@
 import com.google.idea.blaze.base.model.primitives.WorkspaceRoot;
 import com.google.idea.blaze.base.run.BlazeCommandRunConfiguration;
 import com.google.idea.blaze.base.run.BlazeCommandRunConfigurationType;
-import com.google.idea.blaze.base.run.confighandler.BlazeCommandGenericRunConfigurationHandler;
+import com.google.idea.blaze.base.run.state.BlazeCommandRunConfigurationCommonState;
 import com.intellij.execution.actions.ConfigurationContext;
 import com.intellij.openapi.util.Ref;
 import com.intellij.openapi.vfs.VirtualFile;
@@ -56,12 +56,12 @@
     sourceElement.set(dir);
 
     configuration.setTarget(TargetExpression.allFromPackageRecursive(packagePath));
-    BlazeCommandGenericRunConfigurationHandler handler =
-        configuration.getHandlerIfType(BlazeCommandGenericRunConfigurationHandler.class);
-    if (handler == null) {
+    BlazeCommandRunConfigurationCommonState handlerState =
+        configuration.getHandlerStateIfType(BlazeCommandRunConfigurationCommonState.class);
+    if (handlerState == null) {
       return false;
     }
-    handler.setCommand(BlazeCommandName.TEST);
+    handlerState.setCommand(BlazeCommandName.TEST);
     configuration.setGeneratedName();
     return true;
   }
@@ -79,12 +79,12 @@
     if (packagePath == null) {
       return false;
     }
-    BlazeCommandGenericRunConfigurationHandler handler =
-        configuration.getHandlerIfType(BlazeCommandGenericRunConfigurationHandler.class);
-    if (handler == null) {
+    BlazeCommandRunConfigurationCommonState handlerState =
+        configuration.getHandlerStateIfType(BlazeCommandRunConfigurationCommonState.class);
+    if (handlerState == null) {
       return false;
     }
-    return Objects.equals(handler.getCommand(), BlazeCommandName.TEST)
+    return Objects.equals(handlerState.getCommand(), BlazeCommandName.TEST)
         && Objects.equals(
             configuration.getTarget(), TargetExpression.allFromPackageRecursive(packagePath));
   }
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 a28b644..aa00460 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
@@ -16,18 +16,19 @@
 package com.google.idea.blaze.base.run.producers;
 
 import com.google.idea.blaze.base.command.BlazeCommandName;
-import com.google.idea.blaze.base.ideinfo.RuleIdeInfo;
 import com.google.idea.blaze.base.lang.buildfile.psi.FuncallExpression;
+import com.google.idea.blaze.base.lang.buildfile.references.BuildReferenceManager;
 import com.google.idea.blaze.base.model.BlazeProjectData;
 import com.google.idea.blaze.base.model.primitives.Label;
 import com.google.idea.blaze.base.run.BlazeCommandRunConfiguration;
 import com.google.idea.blaze.base.run.BlazeCommandRunConfigurationType;
-import com.google.idea.blaze.base.run.BlazeRuleConfigurationFactory;
-import com.google.idea.blaze.base.run.confighandler.BlazeCommandGenericRunConfigurationHandler;
-import com.google.idea.blaze.base.run.rulefinder.RuleFinder;
+import com.google.idea.blaze.base.run.BlazeRunConfigurationFactory;
+import com.google.idea.blaze.base.run.state.BlazeCommandRunConfigurationCommonState;
 import com.google.idea.blaze.base.sync.data.BlazeProjectDataManager;
-import com.google.idea.blaze.base.sync.projectview.WorkspaceLanguageSettings;
 import com.intellij.execution.actions.ConfigurationContext;
+import com.intellij.execution.configurations.RunConfiguration;
+import com.intellij.openapi.diagnostic.Logger;
+import com.intellij.openapi.project.Project;
 import com.intellij.openapi.util.Ref;
 import com.intellij.psi.PsiElement;
 import com.intellij.psi.util.PsiTreeUtil;
@@ -38,18 +39,18 @@
 public class BlazeBuildFileRunConfigurationProducer
     extends BlazeRunConfigurationProducer<BlazeCommandRunConfiguration> {
 
+  private static final Logger LOG =
+      Logger.getInstance(BlazeBuildFileRunConfigurationProducer.class);
+
   private static class BuildTarget {
     private final FuncallExpression rule;
     private final String ruleType;
     private final Label label;
-    @Nullable private final RuleIdeInfo ruleIdeInfo;
 
-    public BuildTarget(
-        FuncallExpression rule, String ruleType, Label label, @Nullable RuleIdeInfo ruleIdeInfo) {
+    BuildTarget(FuncallExpression rule, String ruleType, Label label) {
       this.rule = rule;
       this.ruleType = ruleType;
       this.label = label;
-      this.ruleIdeInfo = ruleIdeInfo;
     }
   }
 
@@ -67,14 +68,12 @@
     if (blazeProjectData == null) {
       return false;
     }
-    WorkspaceLanguageSettings workspaceLanguageSettings =
-        blazeProjectData.workspaceLanguageSettings;
     BuildTarget target = getBuildTarget(context);
     if (target == null) {
       return false;
     }
     sourceElement.set(target.rule);
-    setupConfiguration(configuration, workspaceLanguageSettings, target);
+    setupConfiguration(configuration.getProject(), blazeProjectData, configuration, target);
     return true;
   }
 
@@ -88,7 +87,6 @@
     if (!Objects.equals(configuration.getTarget(), target.label)) {
       return false;
     }
-
     // We don't know any details about how the various factories set up configurations from here.
     // Simply returning true at this point would be overly broad
     // (all configs with a matching target would be identified).
@@ -105,23 +103,21 @@
     if (blazeProjectData == null) {
       return false;
     }
-    WorkspaceLanguageSettings workspaceLanguageSettings =
-        blazeProjectData.workspaceLanguageSettings;
     BlazeCommandRunConfiguration generatedConfiguration =
         new BlazeCommandRunConfiguration(
             configuration.getProject(), configuration.getFactory(), configuration.getName());
-    setupConfiguration(generatedConfiguration, workspaceLanguageSettings, target);
+    setupConfiguration(
+        configuration.getProject(), blazeProjectData, generatedConfiguration, target);
 
     // TODO This check should be removed once isTestRule is in a RuleFactory and
     // test rules' suggestedName is modified to account for test filter flags.
     if (isTestRule(target.ruleType)) {
-      BlazeCommandGenericRunConfigurationHandler handler =
-          configuration.getHandlerIfType(BlazeCommandGenericRunConfigurationHandler.class);
-      if (handler != null && handler.getTestFilterFlag() != null) {
+      BlazeCommandRunConfigurationCommonState handlerState =
+          configuration.getHandlerStateIfType(BlazeCommandRunConfigurationCommonState.class);
+      if (handlerState != null && handlerState.getTestFilterFlag() != null) {
         return false;
       }
     }
-    // End-TODO
 
     return Objects.equals(configuration.suggestedName(), generatedConfiguration.suggestedName())
         && Objects.equals(
@@ -129,10 +125,27 @@
             generatedConfiguration.getHandler().getCommandName());
   }
 
+  public static boolean handlesTarget(Project project, Label label) {
+    return buildTargetFromLabel(project, label) != null;
+  }
+
+  @Nullable
+  private static BuildTarget buildTargetFromLabel(Project project, Label label) {
+    PsiElement psiElement = BuildReferenceManager.getInstance(project).resolveLabel(label);
+    if (!(psiElement instanceof FuncallExpression)) {
+      return null;
+    }
+    return targetFromFuncall((FuncallExpression) psiElement);
+  }
+
   @Nullable
   private static BuildTarget getBuildTarget(ConfigurationContext context) {
-    FuncallExpression rule =
-        PsiTreeUtil.getNonStrictParentOfType(context.getPsiLocation(), FuncallExpression.class);
+    return targetFromFuncall(
+        PsiTreeUtil.getNonStrictParentOfType(context.getPsiLocation(), FuncallExpression.class));
+  }
+
+  @Nullable
+  private static BuildTarget targetFromFuncall(@Nullable FuncallExpression rule) {
     if (rule == null) {
       return null;
     }
@@ -141,40 +154,52 @@
     if (ruleType == null || label == null) {
       return null;
     }
-    RuleIdeInfo ruleIdeInfo = RuleFinder.getInstance().ruleForTarget(context.getProject(), label);
-    return new BuildTarget(rule, ruleType, label, ruleIdeInfo);
+    return new BuildTarget(rule, ruleType, label);
+  }
+
+  public static void setupConfiguration(RunConfiguration configuration, Label label) {
+    BuildTarget target = buildTargetFromLabel(configuration.getProject(), label);
+    if (target == null || !(configuration instanceof BlazeCommandRunConfiguration)) {
+      LOG.error("Configuration not handled by BUILD file config producer: " + configuration);
+      return;
+    }
+    setupBuildFileConfiguration((BlazeCommandRunConfiguration) configuration, target);
   }
 
   private static void setupConfiguration(
+      Project project,
+      BlazeProjectData blazeProjectData,
       BlazeCommandRunConfiguration configuration,
-      WorkspaceLanguageSettings workspaceLanguageSettings,
       BuildTarget target) {
-    // First see if a BlazeRuleConfigurationFactory can give us a specialized setup.
-    if (target.ruleIdeInfo != null) {
-      for (BlazeRuleConfigurationFactory configurationFactory :
-          BlazeRuleConfigurationFactory.EP_NAME.getExtensions()) {
-        if (configurationFactory.handlesRule(workspaceLanguageSettings, target.ruleIdeInfo)
-            && configurationFactory.handlesConfiguration(configuration)) {
-          configurationFactory.setupConfiguration(configuration, target.ruleIdeInfo);
-          return;
-        }
+    // First see if a BlazeRunConfigurationFactory can give us a specialized setup.
+    for (BlazeRunConfigurationFactory configurationFactory :
+        BlazeRunConfigurationFactory.EP_NAME.getExtensions()) {
+      if (configurationFactory.handlesTarget(project, blazeProjectData, target.label)
+          && configurationFactory.handlesConfiguration(configuration)) {
+        configurationFactory.setupConfiguration(configuration, target.label);
+        return;
       }
     }
 
     // If no factory exists, directly set up the configuration.
+    setupBuildFileConfiguration(configuration, target);
+  }
+
+  private static void setupBuildFileConfiguration(
+      BlazeCommandRunConfiguration configuration, BuildTarget target) {
     configuration.setTarget(target.label);
     // Try to make it a 'blaze build' command, if applicable.
-    BlazeCommandGenericRunConfigurationHandler handler =
-        configuration.getHandlerIfType(BlazeCommandGenericRunConfigurationHandler.class);
-    if (handler != null) {
-      // TODO move the old test rule functionality to a BlazeRuleConfigurationFactory
-      handler.setCommand(
+    BlazeCommandRunConfigurationCommonState handlerState =
+        configuration.getHandlerStateIfType(BlazeCommandRunConfigurationCommonState.class);
+    if (handlerState != null) {
+      // TODO move the old test rule functionality to a BlazeRunConfigurationFactory
+      handlerState.setCommand(
           isTestRule(target.ruleType) ? BlazeCommandName.TEST : BlazeCommandName.BUILD);
     }
     configuration.setGeneratedName();
   }
 
-  // TODO this functionality should be moved to a BlazeRuleConfigurationFactory
+  // TODO this functionality should be moved to a BlazeRunConfigurationFactory
   private static boolean isTestRule(String ruleType) {
     return isTestSuite(ruleType) || ruleType.endsWith("_test");
   }
diff --git a/base/src/com/google/idea/blaze/base/run/rulefinder/RuleFinder.java b/base/src/com/google/idea/blaze/base/run/rulefinder/RuleFinder.java
index 1b1b858..1e48ec0 100644
--- a/base/src/com/google/idea/blaze/base/run/rulefinder/RuleFinder.java
+++ b/base/src/com/google/idea/blaze/base/run/rulefinder/RuleFinder.java
@@ -35,7 +35,7 @@
 
   @Nullable
   public RuleIdeInfo ruleForTarget(Project project, final Label target) {
-    return findRule(project, input -> input.label.equals(target));
+    return findRule(project, rule -> rule.label.equals(target));
   }
 
   public ImmutableList<RuleIdeInfo> rulesOfKinds(Project project, final Kind... kinds) {
@@ -43,7 +43,7 @@
   }
 
   public ImmutableList<RuleIdeInfo> rulesOfKinds(Project project, final List<Kind> kinds) {
-    return ImmutableList.copyOf(findRules(project, input -> input.kindIsOneOf(kinds)));
+    return ImmutableList.copyOf(findRules(project, rule -> rule.kindIsOneOf(kinds)));
   }
 
   @Nullable
diff --git a/base/src/com/google/idea/blaze/base/run/state/BlazeBinaryState.java b/base/src/com/google/idea/blaze/base/run/state/BlazeBinaryState.java
new file mode 100644
index 0000000..2ddeffc
--- /dev/null
+++ b/base/src/com/google/idea/blaze/base/run/state/BlazeBinaryState.java
@@ -0,0 +1,91 @@
+/*
+ * Copyright 2016 The Bazel Authors. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.idea.blaze.base.run.state;
+
+import com.google.common.base.Strings;
+import com.google.idea.blaze.base.settings.Blaze;
+import com.google.idea.blaze.base.ui.UiUtil;
+import com.intellij.openapi.project.Project;
+import com.intellij.ui.components.JBTextField;
+import javax.annotation.Nullable;
+import javax.swing.JComponent;
+import javax.swing.JLabel;
+import org.jdom.Element;
+
+/** State for a Blaze binary to run configurations with. */
+public final class BlazeBinaryState implements RunConfigurationState {
+  private static final String BLAZE_BINARY_ATTR = "blaze-binary";
+
+  @Nullable private String blazeBinary;
+
+  public BlazeBinaryState() {}
+
+  @Nullable
+  public String getBlazeBinary() {
+    return blazeBinary;
+  }
+
+  public void setBlazeBinary(@Nullable String blazeBinary) {
+    this.blazeBinary = blazeBinary;
+  }
+
+  @Override
+  public void readExternal(Element element) {
+    blazeBinary = element.getAttributeValue(BLAZE_BINARY_ATTR);
+  }
+
+  @Override
+  public void writeExternal(Element element) {
+    if (!Strings.isNullOrEmpty(blazeBinary)) {
+      element.setAttribute(BLAZE_BINARY_ATTR, blazeBinary);
+    } else {
+      element.removeAttribute(BLAZE_BINARY_ATTR);
+    }
+  }
+
+  @Override
+  public RunConfigurationStateEditor getEditor(Project project) {
+    return new BlazeBinaryStateEditor(project);
+  }
+
+  private static class BlazeBinaryStateEditor implements RunConfigurationStateEditor {
+    private final String buildSystemName;
+
+    private final JBTextField blazeBinaryField = new JBTextField(1);
+
+    BlazeBinaryStateEditor(Project project) {
+      buildSystemName = Blaze.buildSystemName(project);
+      blazeBinaryField.getEmptyText().setText("(Use global)");
+    }
+
+    @Override
+    public void resetEditorFrom(RunConfigurationState genericState) {
+      BlazeBinaryState state = (BlazeBinaryState) genericState;
+      blazeBinaryField.setText(Strings.nullToEmpty(state.getBlazeBinary()));
+    }
+
+    @Override
+    public void applyEditorTo(RunConfigurationState genericState) {
+      BlazeBinaryState state = (BlazeBinaryState) genericState;
+      state.setBlazeBinary(Strings.emptyToNull(blazeBinaryField.getText()));
+    }
+
+    @Override
+    public JComponent createComponent() {
+      return UiUtil.createBox(new JLabel(buildSystemName + " binary:"), blazeBinaryField);
+    }
+  }
+}
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
new file mode 100644
index 0000000..ab44e47
--- /dev/null
+++ b/base/src/com/google/idea/blaze/base/run/state/BlazeCommandRunConfigurationCommonState.java
@@ -0,0 +1,103 @@
+/*
+ * Copyright 2016 The Bazel Authors. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.idea.blaze.base.run.state;
+
+import com.google.idea.blaze.base.command.BlazeCommandName;
+import com.google.idea.blaze.base.command.BlazeFlags;
+import com.intellij.execution.configurations.RuntimeConfigurationError;
+import com.intellij.execution.configurations.RuntimeConfigurationException;
+import java.io.File;
+import java.util.List;
+import javax.annotation.Nullable;
+
+/**
+ * Shared state common to several {@link
+ * com.google.idea.blaze.base.run.confighandler.BlazeCommandRunConfigurationHandler} types.
+ */
+public final class BlazeCommandRunConfigurationCommonState extends RunConfigurationCompositeState {
+  private static final String USER_BLAZE_FLAG_TAG = "blaze-user-flag";
+  private static final String USER_EXE_FLAG_TAG = "blaze-user-exe-flag";
+
+  private final BlazeCommandState command;
+  private final RunConfigurationFlagsState blazeFlags;
+  private final RunConfigurationFlagsState exeFlags;
+  private final BlazeBinaryState blazeBinary;
+
+  public BlazeCommandRunConfigurationCommonState(String buildSystemName) {
+    command = new BlazeCommandState();
+    blazeFlags = new RunConfigurationFlagsState(USER_BLAZE_FLAG_TAG, buildSystemName + " flags:");
+    exeFlags = new RunConfigurationFlagsState(USER_EXE_FLAG_TAG, "Executable flags:");
+    blazeBinary = new BlazeBinaryState();
+    addStates(command, blazeFlags, exeFlags, blazeBinary);
+  }
+
+  @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();
+  }
+
+  /** @return The list of executable flags the user specified manually. */
+  public List<String> getExeFlags() {
+    return exeFlags.getFlags();
+  }
+
+  @Nullable
+  public String getBlazeBinary() {
+    return blazeBinary.getBlazeBinary();
+  }
+
+  public void setCommand(@Nullable BlazeCommandName command) {
+    this.command.setCommand(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);
+  }
+
+  /** Searches through all blaze flags for the first one beginning with '--test_filter' */
+  @Nullable
+  public String getTestFilterFlag() {
+    for (String flag : getBlazeFlags()) {
+      if (flag.startsWith(BlazeFlags.TEST_FILTER)) {
+        return flag;
+      }
+    }
+    return null;
+  }
+
+  public void validate(String buildSystemName) throws RuntimeConfigurationException {
+    if (getCommand() == null) {
+      throw new RuntimeConfigurationError("You must specify a command.");
+    }
+    String blazeBinaryString = getBlazeBinary();
+    if (blazeBinaryString != null && !(new File(blazeBinaryString).exists())) {
+      throw new RuntimeConfigurationError(buildSystemName + " binary does not exist");
+    }
+  }
+}
diff --git a/base/src/com/google/idea/blaze/base/run/state/BlazeCommandState.java b/base/src/com/google/idea/blaze/base/run/state/BlazeCommandState.java
new file mode 100644
index 0000000..491acb7
--- /dev/null
+++ b/base/src/com/google/idea/blaze/base/run/state/BlazeCommandState.java
@@ -0,0 +1,106 @@
+/*
+ * Copyright 2016 The Bazel Authors. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.idea.blaze.base.run.state;
+
+import com.google.common.base.Strings;
+import com.google.idea.blaze.base.command.BlazeCommandName;
+import com.google.idea.blaze.base.settings.Blaze;
+import com.google.idea.blaze.base.ui.UiUtil;
+import com.intellij.openapi.project.Project;
+import com.intellij.openapi.ui.ComboBox;
+import javax.annotation.Nullable;
+import javax.swing.DefaultComboBoxModel;
+import javax.swing.JComponent;
+import javax.swing.JLabel;
+import org.jdom.Element;
+
+/** State for a {@link BlazeCommandName}. */
+public final class BlazeCommandState implements RunConfigurationState {
+  private static final String COMMAND_ATTR = "blaze-command";
+
+  @Nullable private BlazeCommandName command;
+
+  public BlazeCommandState() {}
+
+  @Nullable
+  public BlazeCommandName getCommand() {
+    return command;
+  }
+
+  public void setCommand(@Nullable BlazeCommandName command) {
+    this.command = command;
+  }
+
+  @Override
+  public void readExternal(Element element) {
+    String commandString = element.getAttributeValue(COMMAND_ATTR);
+    command =
+        Strings.isNullOrEmpty(commandString) ? null : BlazeCommandName.fromString(commandString);
+  }
+
+  @Override
+  public void writeExternal(Element element) {
+    if (command != null) {
+      element.setAttribute(COMMAND_ATTR, command.toString());
+    } else {
+      element.removeAttribute(COMMAND_ATTR);
+    }
+  }
+
+  @Override
+  public RunConfigurationStateEditor getEditor(Project project) {
+    return new BlazeCommandStateEditor(project);
+  }
+
+  private static class BlazeCommandStateEditor implements RunConfigurationStateEditor {
+    private final String buildSystemName;
+
+    private final ComboBox commandCombo;
+
+    BlazeCommandStateEditor(Project project) {
+      buildSystemName = Blaze.buildSystemName(project);
+      commandCombo =
+          new ComboBox(new DefaultComboBoxModel<>(BlazeCommandName.knownCommands().toArray()));
+      // Allow the user to manually specify an unlisted command.
+      commandCombo.setEditable(true);
+    }
+
+    @Override
+    public void resetEditorFrom(RunConfigurationState genericState) {
+      BlazeCommandState state = (BlazeCommandState) genericState;
+      commandCombo.setSelectedItem(state.getCommand());
+    }
+
+    @Override
+    public void applyEditorTo(RunConfigurationState genericState) {
+      BlazeCommandState state = (BlazeCommandState) genericState;
+      Object selectedCommand = commandCombo.getSelectedItem();
+      if (selectedCommand instanceof BlazeCommandName) {
+        state.setCommand((BlazeCommandName) selectedCommand);
+      } else {
+        state.setCommand(
+            Strings.isNullOrEmpty((String) selectedCommand)
+                ? null
+                : BlazeCommandName.fromString(selectedCommand.toString()));
+      }
+    }
+
+    @Override
+    public JComponent createComponent() {
+      return UiUtil.createBox(new JLabel(buildSystemName + " command:"), commandCombo);
+    }
+  }
+}
diff --git a/base/src/com/google/idea/blaze/base/run/state/RunConfigurationCompositeState.java b/base/src/com/google/idea/blaze/base/run/state/RunConfigurationCompositeState.java
new file mode 100644
index 0000000..077e02d
--- /dev/null
+++ b/base/src/com/google/idea/blaze/base/run/state/RunConfigurationCompositeState.java
@@ -0,0 +1,100 @@
+/*
+ * Copyright 2016 The Bazel Authors. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.idea.blaze.base.run.state;
+
+import com.google.common.collect.Lists;
+import com.google.idea.blaze.base.ui.UiUtil;
+import com.intellij.openapi.project.Project;
+import com.intellij.openapi.util.InvalidDataException;
+import com.intellij.openapi.util.WriteExternalException;
+import java.util.Collections;
+import java.util.List;
+import java.util.stream.Collectors;
+import javax.swing.JComponent;
+import org.jdom.Element;
+
+/** Helper class for managing composite state. */
+public class RunConfigurationCompositeState implements RunConfigurationState {
+  private final List<RunConfigurationState> states;
+
+  public RunConfigurationCompositeState(List<RunConfigurationState> states) {
+    this.states = states;
+  }
+
+  protected RunConfigurationCompositeState() {
+    this.states = Lists.newArrayList();
+  }
+
+  protected void addStates(RunConfigurationState... states) {
+    Collections.addAll(this.states, states);
+  }
+
+  @Override
+  public final void readExternal(Element element) throws InvalidDataException {
+    for (RunConfigurationState state : states) {
+      state.readExternal(element);
+    }
+  }
+
+  /** Updates the element with the handler's state. */
+  @Override
+  @SuppressWarnings("ThrowsUncheckedException")
+  public final void writeExternal(Element element) throws WriteExternalException {
+    for (RunConfigurationState state : states) {
+      state.writeExternal(element);
+    }
+  }
+
+  /** @return A {@link RunConfigurationStateEditor} for this state. */
+  @Override
+  public final RunConfigurationStateEditor getEditor(Project project) {
+    return new RunConfigurationCompositeStateEditor(project, states);
+  }
+
+  private static class RunConfigurationCompositeStateEditor implements RunConfigurationStateEditor {
+    List<RunConfigurationStateEditor> editors;
+
+    public RunConfigurationCompositeStateEditor(
+        Project project, List<RunConfigurationState> states) {
+      editors = states.stream().map(state -> state.getEditor(project)).collect(Collectors.toList());
+    }
+
+    @Override
+    public void resetEditorFrom(RunConfigurationState genericState) {
+      RunConfigurationCompositeState state = (RunConfigurationCompositeState) genericState;
+      for (int i = 0; i < editors.size(); ++i) {
+        editors.get(i).resetEditorFrom(state.states.get(i));
+      }
+    }
+
+    @Override
+    public void applyEditorTo(RunConfigurationState genericState) {
+      RunConfigurationCompositeState state = (RunConfigurationCompositeState) genericState;
+      for (int i = 0; i < editors.size(); ++i) {
+        editors.get(i).applyEditorTo(state.states.get(i));
+      }
+    }
+
+    @Override
+    public JComponent createComponent() {
+      return UiUtil.createBox(
+          editors
+              .stream()
+              .map(RunConfigurationStateEditor::createComponent)
+              .collect(Collectors.toList()));
+    }
+  }
+}
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
new file mode 100644
index 0000000..c0edaa4
--- /dev/null
+++ b/base/src/com/google/idea/blaze/base/run/state/RunConfigurationFlagsState.java
@@ -0,0 +1,128 @@
+/*
+ * Copyright 2016 The Bazel Authors. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.idea.blaze.base.run.state;
+
+import com.google.common.base.Strings;
+import com.google.common.collect.ImmutableList;
+import com.google.idea.blaze.base.ui.UiUtil;
+import com.intellij.openapi.project.Project;
+import com.intellij.util.execution.ParametersListUtil;
+import java.util.List;
+import javax.swing.JComponent;
+import javax.swing.JLabel;
+import javax.swing.JScrollPane;
+import javax.swing.JTextArea;
+import javax.swing.ScrollPaneConstants;
+import org.jdom.Element;
+
+/** State for a list of user-defined flags. */
+public final class RunConfigurationFlagsState implements RunConfigurationState {
+
+  private final String tag;
+  private final String fieldLabel;
+
+  private ImmutableList<String> flags = ImmutableList.of();
+
+  public RunConfigurationFlagsState(String tag, String fieldLabel) {
+    this.tag = tag;
+    this.fieldLabel = fieldLabel;
+  }
+
+  public List<String> getFlags() {
+    return flags;
+  }
+
+  public void setFlags(List<String> flags) {
+    this.flags = ImmutableList.copyOf(flags);
+  }
+
+  @Override
+  public void readExternal(Element element) {
+    ImmutableList.Builder<String> flagsBuilder = ImmutableList.builder();
+    for (Element e : element.getChildren(tag)) {
+      String flag = e.getTextTrim();
+      if (flag != null && !flag.isEmpty()) {
+        flagsBuilder.add(flag);
+      }
+    }
+    flags = flagsBuilder.build();
+  }
+
+  @Override
+  public void writeExternal(Element element) {
+    element.removeChildren(tag);
+    for (String flag : flags) {
+      Element child = new Element(tag);
+      child.setText(flag);
+      element.addContent(child);
+    }
+  }
+
+  @Override
+  public RunConfigurationStateEditor getEditor(Project project) {
+    return new RunConfigurationFlagsStateEditor(fieldLabel);
+  }
+
+  private static class RunConfigurationFlagsStateEditor implements RunConfigurationStateEditor {
+
+    private final JTextArea flagsField = new JTextArea(5, 1);
+    private final String fieldLabel;
+
+    RunConfigurationFlagsStateEditor(String fieldLabel) {
+      this.fieldLabel = fieldLabel;
+    }
+
+    private static String makeFlagString(List<String> flags) {
+      StringBuilder flagString = new StringBuilder();
+      for (String flag : flags) {
+        if (flagString.length() > 0) {
+          flagString.append('\n');
+        }
+        if (flag.isEmpty() || flag.contains(" ") || flag.contains("|")) {
+          flagString.append('"');
+          flagString.append(flag);
+          flagString.append('"');
+        } else {
+          flagString.append(flag);
+        }
+      }
+      return flagString.toString();
+    }
+
+    @Override
+    public void resetEditorFrom(RunConfigurationState genericState) {
+      RunConfigurationFlagsState state = (RunConfigurationFlagsState) genericState;
+      // Normally we could just use ParametersListUtils.join, but that will only space-delimit args.
+      flagsField.setText(makeFlagString(state.getFlags()));
+    }
+
+    @Override
+    public void applyEditorTo(RunConfigurationState genericState) {
+      RunConfigurationFlagsState state = (RunConfigurationFlagsState) genericState;
+      state.setFlags(ParametersListUtil.parse(Strings.nullToEmpty(flagsField.getText())));
+    }
+
+    @Override
+    public JComponent createComponent() {
+      return UiUtil.createBox(
+          new JLabel(fieldLabel),
+          new JScrollPane(
+              flagsField,
+              JScrollPane.VERTICAL_SCROLLBAR_AS_NEEDED,
+              ScrollPaneConstants.HORIZONTAL_SCROLLBAR_AS_NEEDED));
+    }
+  }
+}
diff --git a/base/src/com/google/idea/blaze/base/run/state/RunConfigurationState.java b/base/src/com/google/idea/blaze/base/run/state/RunConfigurationState.java
new file mode 100644
index 0000000..8a01f01
--- /dev/null
+++ b/base/src/com/google/idea/blaze/base/run/state/RunConfigurationState.java
@@ -0,0 +1,35 @@
+/*
+ * Copyright 2016 The Bazel Authors. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.idea.blaze.base.run.state;
+
+import com.intellij.openapi.project.Project;
+import com.intellij.openapi.util.InvalidDataException;
+import com.intellij.openapi.util.WriteExternalException;
+import org.jdom.Element;
+
+/** Supports managing part of a run configuration's state. */
+public interface RunConfigurationState {
+
+  /** Loads this handler's state from the external data. */
+  void readExternal(Element element) throws InvalidDataException;
+
+  /** Updates the element with the handler's state. */
+  @SuppressWarnings("ThrowsUncheckedException")
+  void writeExternal(Element element) throws WriteExternalException;
+
+  /** @return A {@link RunConfigurationStateEditor} for this state. */
+  RunConfigurationStateEditor getEditor(Project project);
+}
diff --git a/base/src/com/google/idea/blaze/base/run/state/RunConfigurationStateEditor.java b/base/src/com/google/idea/blaze/base/run/state/RunConfigurationStateEditor.java
new file mode 100644
index 0000000..b04020e
--- /dev/null
+++ b/base/src/com/google/idea/blaze/base/run/state/RunConfigurationStateEditor.java
@@ -0,0 +1,31 @@
+/*
+ * Copyright 2016 The Bazel Authors. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.idea.blaze.base.run.state;
+
+import javax.swing.JComponent;
+
+/** Provides support for editing {@link RunConfigurationState}s. */
+public interface RunConfigurationStateEditor {
+
+  /** Reset the editor based on the given state. */
+  void resetEditorFrom(RunConfigurationState state);
+
+  /** Update the given state based on the editor. */
+  void applyEditorTo(RunConfigurationState state);
+
+  /** @return A component to display for the editor. */
+  JComponent createComponent();
+}
diff --git a/base/src/com/google/idea/blaze/base/run/testmap/TestRuleFinderImpl.java b/base/src/com/google/idea/blaze/base/run/testmap/TestRuleFinderImpl.java
index 258f059..9f51c32 100644
--- a/base/src/com/google/idea/blaze/base/run/testmap/TestRuleFinderImpl.java
+++ b/base/src/com/google/idea/blaze/base/run/testmap/TestRuleFinderImpl.java
@@ -25,15 +25,18 @@
 import com.google.common.collect.Sets;
 import com.google.idea.blaze.base.ideinfo.ArtifactLocation;
 import com.google.idea.blaze.base.ideinfo.RuleIdeInfo;
+import com.google.idea.blaze.base.ideinfo.RuleKey;
+import com.google.idea.blaze.base.ideinfo.RuleMap;
 import com.google.idea.blaze.base.model.BlazeProjectData;
-import com.google.idea.blaze.base.model.RuleMap;
 import com.google.idea.blaze.base.model.primitives.Kind;
 import com.google.idea.blaze.base.model.primitives.Label;
 import com.google.idea.blaze.base.projectview.ProjectViewSet;
 import com.google.idea.blaze.base.run.TestRuleFinder;
+import com.google.idea.blaze.base.scope.BlazeContext;
 import com.google.idea.blaze.base.settings.BlazeImportSettings;
 import com.google.idea.blaze.base.sync.SyncListener;
 import com.google.idea.blaze.base.sync.data.BlazeProjectDataManager;
+import com.google.idea.blaze.base.sync.workspace.ArtifactLocationDecoder;
 import com.intellij.openapi.project.Project;
 import java.io.File;
 import java.util.Collection;
@@ -55,12 +58,12 @@
 
   static class TestMap {
     private final Project project;
-    private final Multimap<File, Label> rootsMap;
+    private final Multimap<File, RuleKey> rootsMap;
     private final RuleMap ruleMap;
 
-    TestMap(Project project, RuleMap ruleMap) {
+    TestMap(Project project, ArtifactLocationDecoder artifactLocationDecoder, RuleMap ruleMap) {
       this.project = project;
-      this.rootsMap = createRootsMap(ruleMap.rules());
+      this.rootsMap = createRootsMap(artifactLocationDecoder, ruleMap.rules());
       this.ruleMap = ruleMap;
     }
 
@@ -75,45 +78,47 @@
 
     @VisibleForTesting
     Collection<Label> testTargetsForSourceFile(
-        ImmutableMultimap<Label, Label> rdepsMap, File sourceFile) {
+        ImmutableMultimap<RuleKey, RuleKey> rdepsMap, File sourceFile) {
       return testRulesForSourceFile(rdepsMap, sourceFile)
           .stream()
-          .map((rule) -> rule.label)
+          .filter(RuleIdeInfo::isPlainTarget)
+          .map(rule -> rule.label)
           .collect(Collectors.toList());
     }
 
     Collection<RuleIdeInfo> testRulesForSourceFile(
-        ImmutableMultimap<Label, Label> rdepsMap, File sourceFile) {
+        ImmutableMultimap<RuleKey, RuleKey> rdepsMap, File sourceFile) {
       List<RuleIdeInfo> result = Lists.newArrayList();
-      Collection<Label> roots = rootsMap.get(sourceFile);
+      Collection<RuleKey> roots = rootsMap.get(sourceFile);
 
-      Queue<Label> todo = Queues.newArrayDeque();
-      for (Label label : roots) {
+      Queue<RuleKey> todo = Queues.newArrayDeque();
+      for (RuleKey label : roots) {
         todo.add(label);
       }
-      Set<Label> seen = Sets.newHashSet();
+      Set<RuleKey> seen = Sets.newHashSet();
       while (!todo.isEmpty()) {
-        Label label = todo.remove();
-        if (!seen.add(label)) {
+        RuleKey ruleKey = todo.remove();
+        if (!seen.add(ruleKey)) {
           continue;
         }
 
-        RuleIdeInfo rule = ruleMap.get(label);
+        RuleIdeInfo rule = ruleMap.get(ruleKey);
         if (isTestRule(rule)) {
           result.add(rule);
         }
-        for (Label rdep : rdepsMap.get(label)) {
+        for (RuleKey rdep : rdepsMap.get(ruleKey)) {
           todo.add(rdep);
         }
       }
       return result;
     }
 
-    static Multimap<File, Label> createRootsMap(Collection<RuleIdeInfo> rules) {
-      Multimap<File, Label> result = ArrayListMultimap.create();
+    static Multimap<File, RuleKey> createRootsMap(
+        ArtifactLocationDecoder artifactLocationDecoder, Collection<RuleIdeInfo> rules) {
+      Multimap<File, RuleKey> result = ArrayListMultimap.create();
       for (RuleIdeInfo ruleIdeInfo : rules) {
         for (ArtifactLocation source : ruleIdeInfo.sources) {
-          result.put(source.getFile(), ruleIdeInfo.label);
+          result.put(artifactLocationDecoder.decode(source), ruleIdeInfo.key);
         }
       }
       return result;
@@ -158,7 +163,7 @@
     if (blazeProjectData == null) {
       return null;
     }
-    return new TestMap(project, blazeProjectData.ruleMap);
+    return new TestMap(project, blazeProjectData.artifactLocationDecoder, blazeProjectData.ruleMap);
   }
 
   private synchronized void clearMapData() {
@@ -169,6 +174,7 @@
     @Override
     public void onSyncComplete(
         Project project,
+        BlazeContext context,
         BlazeImportSettings importSettings,
         ProjectViewSet projectViewSet,
         BlazeProjectData blazeProjectData,
diff --git a/base/src/com/google/idea/blaze/base/sync/BlazeSyncPlugin.java b/base/src/com/google/idea/blaze/base/sync/BlazeSyncPlugin.java
index 985ff17..4c7d64e 100644
--- a/base/src/com/google/idea/blaze/base/sync/BlazeSyncPlugin.java
+++ b/base/src/com/google/idea/blaze/base/sync/BlazeSyncPlugin.java
@@ -17,8 +17,8 @@
 
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableSet;
+import com.google.idea.blaze.base.ideinfo.RuleMap;
 import com.google.idea.blaze.base.model.BlazeProjectData;
-import com.google.idea.blaze.base.model.RuleMap;
 import com.google.idea.blaze.base.model.SyncState;
 import com.google.idea.blaze.base.model.primitives.LanguageClass;
 import com.google.idea.blaze.base.model.primitives.WorkspaceRoot;
@@ -27,6 +27,7 @@
 import com.google.idea.blaze.base.projectview.section.SectionParser;
 import com.google.idea.blaze.base.scope.BlazeContext;
 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;
@@ -99,6 +100,7 @@
       BlazeRoots blazeRoots,
       @Nullable WorkingSet workingSet,
       WorkspacePathResolver workspacePathResolver,
+      ArtifactLocationDecoder artifactLocationDecoder,
       RuleMap ruleMap,
       SyncState.Builder syncStateBuilder,
       @Nullable SyncState previousSyncState);
@@ -183,6 +185,7 @@
         BlazeRoots blazeRoots,
         @Nullable WorkingSet workingSet,
         WorkspacePathResolver workspacePathResolver,
+        ArtifactLocationDecoder artifactLocationDecoder,
         RuleMap ruleMap,
         SyncState.Builder syncStateBuilder,
         @Nullable SyncState previousSyncState) {}
diff --git a/base/src/com/google/idea/blaze/base/sync/BlazeSyncTask.java b/base/src/com/google/idea/blaze/base/sync/BlazeSyncTask.java
index a49b8bf..6b5c4dc 100755
--- a/base/src/com/google/idea/blaze/base/sync/BlazeSyncTask.java
+++ b/base/src/com/google/idea/blaze/base/sync/BlazeSyncTask.java
@@ -26,11 +26,11 @@
 import com.google.idea.blaze.base.async.executor.BlazeExecutor;
 import com.google.idea.blaze.base.experiments.ExperimentScope;
 import com.google.idea.blaze.base.filecache.FileCaches;
+import com.google.idea.blaze.base.ideinfo.RuleKey;
+import com.google.idea.blaze.base.ideinfo.RuleMap;
 import com.google.idea.blaze.base.metrics.Action;
 import com.google.idea.blaze.base.model.BlazeProjectData;
-import com.google.idea.blaze.base.model.RuleMap;
 import com.google.idea.blaze.base.model.SyncState;
-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.WorkspacePath;
 import com.google.idea.blaze.base.model.primitives.WorkspaceRoot;
@@ -69,6 +69,7 @@
 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.ArtifactLocationDecoder;
+import com.google.idea.blaze.base.sync.workspace.ArtifactLocationDecoderImpl;
 import com.google.idea.blaze.base.sync.workspace.BlazeRoots;
 import com.google.idea.blaze.base.sync.workspace.WorkingSet;
 import com.google.idea.blaze.base.sync.workspace.WorkspacePathResolver;
@@ -149,13 +150,13 @@
     SyncResult syncResult = SyncResult.FAILURE;
     try {
       SaveUtil.saveAllFiles();
-      onSyncStart(project);
+      onSyncStart(project, context);
       syncResult = doSyncProject(context);
     } catch (AssertionError | Exception e) {
       LOG.error(e);
       IssueOutput.error("Internal error: " + e.getMessage()).submit(context);
     } finally {
-      afterSync(project, syncResult);
+      afterSync(project, context, syncResult);
     }
     return syncResult == SyncResult.SUCCESS || syncResult == SyncResult.PARTIAL_SUCCESS;
   }
@@ -215,6 +216,8 @@
     }
     WorkspacePathResolver workspacePathResolver =
         workspacePathResolverAndProjectView.workspacePathResolver;
+    ArtifactLocationDecoder artifactLocationDecoder =
+        new ArtifactLocationDecoderImpl(blazeRoots, workspacePathResolver);
     ProjectViewSet projectViewSet = workspacePathResolverAndProjectView.projectViewSet;
 
     WorkspaceLanguageSettings workspaceLanguageSettings =
@@ -277,7 +280,7 @@
               projectViewSet,
               targets,
               workspaceLanguageSettings,
-              new ArtifactLocationDecoder(blazeRoots, workspacePathResolver),
+              artifactLocationDecoder,
               syncStateBuilder,
               previousSyncState,
               mergeWithOldState);
@@ -292,7 +295,7 @@
       RuleMap ruleMap = ideQueryResult.ruleMap;
       ideInfoResult = ideQueryResult.buildResult;
 
-      ListenableFuture<ImmutableMultimap<Label, Label>> reverseDependenciesFuture =
+      ListenableFuture<ImmutableMultimap<RuleKey, RuleKey>> reverseDependenciesFuture =
           BlazeExecutor.getInstance().submit(() -> ReverseDependencyMap.createRdepsMap(ruleMap));
 
       boolean doResolve = syncPluginRequiresBuild || oldBlazeProjectData == null;
@@ -322,13 +325,14 @@
                   blazeRoots,
                   workingSet,
                   workspacePathResolver,
+                  artifactLocationDecoder,
                   ruleMap,
                   syncStateBuilder,
                   previousSyncState);
             }
           });
 
-      ImmutableMultimap<Label, Label> reverseDependencies =
+      ImmutableMultimap<RuleKey, RuleKey> reverseDependencies =
           FutureUtil.waitForFuture(context, reverseDependenciesFuture)
               .timed("ReverseDependencies")
               .onError("Failed to compute reverse dependency map")
@@ -345,6 +349,7 @@
               blazeRoots,
               workingSet,
               workspacePathResolver,
+              artifactLocationDecoder,
               workspaceLanguageSettings,
               syncStateBuilder.build(),
               reverseDependencies,
@@ -685,17 +690,17 @@
     return VirtualFileManager.constructUrl(StandardFileSystems.FILE_PROTOCOL, filePath);
   }
 
-  private static void onSyncStart(Project project) {
+  private static void onSyncStart(Project project, BlazeContext context) {
     final SyncListener[] syncListeners = SyncListener.EP_NAME.getExtensions();
     for (SyncListener syncListener : syncListeners) {
-      syncListener.onSyncStart(project);
+      syncListener.onSyncStart(project, context);
     }
   }
 
-  private static void afterSync(Project project, SyncResult syncResult) {
+  private static void afterSync(Project project, BlazeContext context, SyncResult syncResult) {
     final SyncListener[] syncListeners = SyncListener.EP_NAME.getExtensions();
     for (SyncListener syncListener : syncListeners) {
-      syncListener.afterSync(project, syncResult);
+      syncListener.afterSync(project, context, syncResult);
     }
   }
 
@@ -710,7 +715,7 @@
     final SyncListener[] syncListeners = SyncListener.EP_NAME.getExtensions();
     for (SyncListener syncListener : syncListeners) {
       syncListener.onSyncComplete(
-          project, importSettings, projectViewSet, blazeProjectData, syncResult);
+          project, context, importSettings, projectViewSet, blazeProjectData, syncResult);
     }
   }
 
diff --git a/base/src/com/google/idea/blaze/base/sync/SyncListener.java b/base/src/com/google/idea/blaze/base/sync/SyncListener.java
index 6445b14..28ba5fc 100644
--- a/base/src/com/google/idea/blaze/base/sync/SyncListener.java
+++ b/base/src/com/google/idea/blaze/base/sync/SyncListener.java
@@ -17,6 +17,7 @@
 
 import com.google.idea.blaze.base.model.BlazeProjectData;
 import com.google.idea.blaze.base.projectview.ProjectViewSet;
+import com.google.idea.blaze.base.scope.BlazeContext;
 import com.google.idea.blaze.base.settings.BlazeImportSettings;
 import com.intellij.openapi.extensions.ExtensionPointName;
 import com.intellij.openapi.project.Project;
@@ -39,34 +40,36 @@
   }
 
   /** Called after open documents have been saved, prior to starting the blaze sync. */
-  void onSyncStart(Project project);
+  void onSyncStart(Project project, BlazeContext context);
 
   /** Called on successful (or partially successful) completion of a sync */
   void onSyncComplete(
       Project project,
+      BlazeContext context,
       BlazeImportSettings importSettings,
       ProjectViewSet projectViewSet,
       BlazeProjectData blazeProjectData,
       SyncResult syncResult);
 
   /** Guaranteed to be called once per sync, regardless of whether it successfully completed */
-  void afterSync(Project project, SyncResult syncResult);
+  void afterSync(Project project, BlazeContext context, SyncResult syncResult);
 
   /** Convenience adapter class. */
   abstract class Adapter implements SyncListener {
 
     @Override
-    public void onSyncStart(Project project) {}
+    public void onSyncStart(Project project, BlazeContext context) {}
 
     @Override
     public void onSyncComplete(
         Project project,
+        BlazeContext context,
         BlazeImportSettings importSettings,
         ProjectViewSet projectViewSet,
         BlazeProjectData blazeProjectData,
         SyncResult syncResult) {}
 
     @Override
-    public void afterSync(Project project, SyncResult syncResult) {}
+    public void afterSync(Project project, BlazeContext context, SyncResult syncResult) {}
   }
 }
diff --git a/base/src/com/google/idea/blaze/base/sync/actions/PartialSyncAction.java b/base/src/com/google/idea/blaze/base/sync/actions/PartialSyncAction.java
index 2d6512b..cba67de 100644
--- a/base/src/com/google/idea/blaze/base/sync/actions/PartialSyncAction.java
+++ b/base/src/com/google/idea/blaze/base/sync/actions/PartialSyncAction.java
@@ -92,7 +92,7 @@
     } else {
       targets.addAll(
           SourceToRuleMap.getInstance(project)
-              .getTargetsForSourceFile(new File(virtualFile.getPath())));
+              .getTargetsToBuildForSourceFile(new File(virtualFile.getPath())));
 
       // If empty, try to build parent package
       if (targets.isEmpty()) {
diff --git a/base/src/com/google/idea/blaze/base/sync/aspects/BlazeIdeInterface.java b/base/src/com/google/idea/blaze/base/sync/aspects/BlazeIdeInterface.java
index d46ac4a..573c4b8 100644
--- a/base/src/com/google/idea/blaze/base/sync/aspects/BlazeIdeInterface.java
+++ b/base/src/com/google/idea/blaze/base/sync/aspects/BlazeIdeInterface.java
@@ -15,7 +15,7 @@
  */
 package com.google.idea.blaze.base.sync.aspects;
 
-import com.google.idea.blaze.base.model.RuleMap;
+import com.google.idea.blaze.base.ideinfo.RuleMap;
 import com.google.idea.blaze.base.model.SyncState;
 import com.google.idea.blaze.base.model.primitives.TargetExpression;
 import com.google.idea.blaze.base.model.primitives.WorkspaceRoot;
diff --git a/base/src/com/google/idea/blaze/base/sync/aspects/BlazeIdeInterfaceAspectsImpl.java b/base/src/com/google/idea/blaze/base/sync/aspects/BlazeIdeInterfaceAspectsImpl.java
index 1b4be0e..936268e 100644
--- a/base/src/com/google/idea/blaze/base/sync/aspects/BlazeIdeInterfaceAspectsImpl.java
+++ b/base/src/com/google/idea/blaze/base/sync/aspects/BlazeIdeInterfaceAspectsImpl.java
@@ -32,11 +32,11 @@
 import com.google.idea.blaze.base.command.ExperimentalShowArtifactsLineProcessor;
 import com.google.idea.blaze.base.filecache.FileDiffer;
 import com.google.idea.blaze.base.ideinfo.RuleIdeInfo;
+import com.google.idea.blaze.base.ideinfo.RuleKey;
+import com.google.idea.blaze.base.ideinfo.RuleMap;
 import com.google.idea.blaze.base.issueparser.IssueOutputLineProcessor;
 import com.google.idea.blaze.base.metrics.Action;
-import com.google.idea.blaze.base.model.RuleMap;
 import com.google.idea.blaze.base.model.SyncState;
-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.prefetch.PrefetchService;
@@ -75,10 +75,10 @@
       new BoolExperiment("ide.info.keep.going", true);
 
   static class State implements Serializable {
-    private static final long serialVersionUID = 13L;
+    private static final long serialVersionUID = 14L;
     RuleMap ruleMap;
     ImmutableMap<File, Long> fileState = null;
-    Map<File, Label> fileToLabel = Maps.newHashMap();
+    Map<File, RuleKey> fileToRuleMapKey = Maps.newHashMap();
     WorkspaceLanguageSettings workspaceLanguageSettings;
     String aspectStrategyName;
   }
@@ -273,19 +273,19 @@
                   state.workspaceLanguageSettings = workspaceLanguageSettings;
                   state.aspectStrategyName = aspectStrategy.getName();
 
-                  Map<Label, RuleIdeInfo> ruleMap = Maps.newHashMap();
-                  Map<Label, RuleIdeInfo> updatedRules = Maps.newHashMap();
+                  Map<RuleKey, RuleIdeInfo> ruleMap = Maps.newHashMap();
+                  Map<RuleKey, RuleIdeInfo> updatedRules = Maps.newHashMap();
                   if (prevState != null) {
                     ruleMap.putAll(prevState.ruleMap.map());
-                    state.fileToLabel.putAll(prevState.fileToLabel);
+                    state.fileToRuleMapKey.putAll(prevState.fileToRuleMapKey);
                   }
 
                   // Update removed unless we're merging with the old state
                   if (!mergeWithOldState) {
                     for (File removedFile : removedFiles) {
-                      Label label = state.fileToLabel.remove(removedFile);
-                      if (label != null) {
-                        ruleMap.remove(label);
+                      RuleKey key = state.fileToRuleMapKey.remove(removedFile);
+                      if (key != null) {
+                        ruleMap.remove(key);
                       }
                     }
                   }
@@ -306,9 +306,7 @@
                                   aspectStrategy.readAspectFile(file);
                               RuleIdeInfo ruleIdeInfo =
                                   IdeInfoFromProtobuf.makeRuleIdeInfo(
-                                      workspaceLanguageSettings,
-                                      artifactLocationDecoder,
-                                      ruleProto);
+                                      workspaceLanguageSettings, ruleProto);
                               return new RuleIdeInfoPair(file, ruleIdeInfo);
                             }));
                   }
@@ -319,11 +317,11 @@
                     for (RuleIdeInfoPair ruleIdeInfoOrSdkInfo : Futures.allAsList(futures).get()) {
                       if (ruleIdeInfoOrSdkInfo.ruleIdeInfo != null) {
                         File file = ruleIdeInfoOrSdkInfo.file;
-                        Label label = ruleIdeInfoOrSdkInfo.ruleIdeInfo.label;
+                        RuleKey key = ruleIdeInfoOrSdkInfo.ruleIdeInfo.key;
                         RuleIdeInfo previousRule =
-                            updatedRules.putIfAbsent(label, ruleIdeInfoOrSdkInfo.ruleIdeInfo);
+                            updatedRules.putIfAbsent(key, ruleIdeInfoOrSdkInfo.ruleIdeInfo);
                         if (previousRule == null) {
-                          state.fileToLabel.put(file, label);
+                          state.fileToRuleMapKey.put(file, key);
                         } else {
                           duplicateRuleLabels++;
                         }
diff --git a/base/src/com/google/idea/blaze/base/sync/aspects/IdeInfoFromProtobuf.java b/base/src/com/google/idea/blaze/base/sync/aspects/IdeInfoFromProtobuf.java
index 4f1e622..812148b 100644
--- a/base/src/com/google/idea/blaze/base/sync/aspects/IdeInfoFromProtobuf.java
+++ b/base/src/com/google/idea/blaze/base/sync/aspects/IdeInfoFromProtobuf.java
@@ -33,7 +33,6 @@
 import com.google.idea.blaze.base.model.primitives.Kind;
 import com.google.idea.blaze.base.model.primitives.Label;
 import com.google.idea.blaze.base.sync.projectview.WorkspaceLanguageSettings;
-import com.google.idea.blaze.base.sync.workspace.ArtifactLocationDecoder;
 import com.google.repackaged.devtools.build.lib.ideinfo.androidstudio.AndroidStudioIdeInfo;
 import com.google.repackaged.protobuf.ProtocolStringList;
 import java.util.Collection;
@@ -46,7 +45,6 @@
   @Nullable
   public static RuleIdeInfo makeRuleIdeInfo(
       WorkspaceLanguageSettings workspaceLanguageSettings,
-      ArtifactLocationDecoder decoder,
       AndroidStudioIdeInfo.RuleIdeInfo message) {
     Kind kind = getKind(message);
     if (kind == null) {
@@ -57,7 +55,7 @@
     }
 
     Label label = new Label(message.getLabel());
-    ArtifactLocation buildFile = getBuildFile(decoder, message);
+    ArtifactLocation buildFile = getBuildFile(message);
 
     Collection<Label> dependencies = makeLabelListFromProtobuf(message.getDependenciesList());
     Collection<Label> runtimeDeps = makeLabelListFromProtobuf(message.getRuntimeDepsList());
@@ -66,7 +64,7 @@
     Collection<ArtifactLocation> sources = Lists.newArrayList();
     CRuleIdeInfo cRuleIdeInfo = null;
     if (message.hasCRuleIdeInfo()) {
-      cRuleIdeInfo = makeCRuleIdeInfo(decoder, message.getCRuleIdeInfo());
+      cRuleIdeInfo = makeCRuleIdeInfo(message.getCRuleIdeInfo());
       sources.addAll(cRuleIdeInfo.sources);
     }
     CToolchainIdeInfo cToolchainIdeInfo = null;
@@ -75,14 +73,14 @@
     }
     JavaRuleIdeInfo javaRuleIdeInfo = null;
     if (message.hasJavaRuleIdeInfo()) {
-      javaRuleIdeInfo = makeJavaRuleIdeInfo(decoder, message.getJavaRuleIdeInfo());
+      javaRuleIdeInfo = makeJavaRuleIdeInfo(message.getJavaRuleIdeInfo());
       Collection<ArtifactLocation> javaSources =
-          makeArtifactLocationList(decoder, message.getJavaRuleIdeInfo().getSourcesList());
+          makeArtifactLocationList(message.getJavaRuleIdeInfo().getSourcesList());
       sources.addAll(javaSources);
     }
     AndroidRuleIdeInfo androidRuleIdeInfo = null;
     if (message.hasAndroidRuleIdeInfo()) {
-      androidRuleIdeInfo = makeAndroidRuleIdeInfo(decoder, message.getAndroidRuleIdeInfo());
+      androidRuleIdeInfo = makeAndroidRuleIdeInfo(message.getAndroidRuleIdeInfo());
     }
     TestIdeInfo testIdeInfo = null;
     if (message.hasTestInfo()) {
@@ -91,7 +89,7 @@
     ProtoLibraryLegacyInfo protoLibraryLegacyInfo = null;
     if (message.hasProtoLibraryLegacyJavaIdeInfo()) {
       protoLibraryLegacyInfo =
-          makeProtoLibraryLegacyInfo(decoder, message.getProtoLibraryLegacyJavaIdeInfo());
+          makeProtoLibraryLegacyInfo(message.getProtoLibraryLegacyJavaIdeInfo());
     }
     JavaToolchainIdeInfo javaToolchainIdeInfo = null;
     if (message.hasJavaToolchainIdeInfo()) {
@@ -116,18 +114,15 @@
   }
 
   @Nullable
-  private static ArtifactLocation getBuildFile(
-      ArtifactLocationDecoder decoder, AndroidStudioIdeInfo.RuleIdeInfo message) {
+  private static ArtifactLocation getBuildFile(AndroidStudioIdeInfo.RuleIdeInfo message) {
     if (message.hasBuildFileArtifactLocation()) {
-      return makeArtifactLocation(decoder, message.getBuildFileArtifactLocation());
+      return makeArtifactLocation(message.getBuildFileArtifactLocation());
     }
     return null;
   }
 
-  private static CRuleIdeInfo makeCRuleIdeInfo(
-      ArtifactLocationDecoder decoder, AndroidStudioIdeInfo.CRuleIdeInfo cRuleIdeInfo) {
-    List<ArtifactLocation> sources =
-        makeArtifactLocationList(decoder, cRuleIdeInfo.getSourceList());
+  private static CRuleIdeInfo makeCRuleIdeInfo(AndroidStudioIdeInfo.CRuleIdeInfo cRuleIdeInfo) {
+    List<ArtifactLocation> sources = makeArtifactLocationList(cRuleIdeInfo.getSourceList());
     List<ExecutionRootPath> transitiveIncludeDirectories =
         makeExecutionRootPathList(cRuleIdeInfo.getTransitiveIncludeDirectoryList());
     List<ExecutionRootPath> transitiveQuoteIncludeDirectories =
@@ -183,35 +178,31 @@
   }
 
   private static JavaRuleIdeInfo makeJavaRuleIdeInfo(
-      ArtifactLocationDecoder decoder, AndroidStudioIdeInfo.JavaRuleIdeInfo javaRuleIdeInfo) {
+      AndroidStudioIdeInfo.JavaRuleIdeInfo javaRuleIdeInfo) {
     return new JavaRuleIdeInfo(
-        makeLibraryArtifactList(decoder, javaRuleIdeInfo.getJarsList()),
-        makeLibraryArtifactList(decoder, javaRuleIdeInfo.getGeneratedJarsList()),
+        makeLibraryArtifactList(javaRuleIdeInfo.getJarsList()),
+        makeLibraryArtifactList(javaRuleIdeInfo.getGeneratedJarsList()),
         javaRuleIdeInfo.hasFilteredGenJar()
-            ? makeLibraryArtifact(decoder, javaRuleIdeInfo.getFilteredGenJar())
+            ? makeLibraryArtifact(javaRuleIdeInfo.getFilteredGenJar())
             : null,
         javaRuleIdeInfo.hasPackageManifest()
-            ? makeArtifactLocation(decoder, javaRuleIdeInfo.getPackageManifest())
+            ? makeArtifactLocation(javaRuleIdeInfo.getPackageManifest())
             : null,
-        javaRuleIdeInfo.hasJdeps()
-            ? makeArtifactLocation(decoder, javaRuleIdeInfo.getJdeps())
-            : null);
+        javaRuleIdeInfo.hasJdeps() ? makeArtifactLocation(javaRuleIdeInfo.getJdeps()) : null);
   }
 
   private static AndroidRuleIdeInfo makeAndroidRuleIdeInfo(
-      ArtifactLocationDecoder decoder, AndroidStudioIdeInfo.AndroidRuleIdeInfo androidRuleIdeInfo) {
+      AndroidStudioIdeInfo.AndroidRuleIdeInfo androidRuleIdeInfo) {
     return new AndroidRuleIdeInfo(
-        makeArtifactLocationList(decoder, androidRuleIdeInfo.getResourcesList()),
+        makeArtifactLocationList(androidRuleIdeInfo.getResourcesList()),
         androidRuleIdeInfo.getJavaPackage(),
         androidRuleIdeInfo.getGenerateResourceClass(),
         androidRuleIdeInfo.hasManifest()
-            ? makeArtifactLocation(decoder, androidRuleIdeInfo.getManifest())
+            ? makeArtifactLocation(androidRuleIdeInfo.getManifest())
             : null,
-        androidRuleIdeInfo.hasIdlJar()
-            ? makeLibraryArtifact(decoder, androidRuleIdeInfo.getIdlJar())
-            : null,
+        androidRuleIdeInfo.hasIdlJar() ? makeLibraryArtifact(androidRuleIdeInfo.getIdlJar()) : null,
         androidRuleIdeInfo.hasResourceJar()
-            ? makeLibraryArtifact(decoder, androidRuleIdeInfo.getResourceJar())
+            ? makeLibraryArtifact(androidRuleIdeInfo.getResourceJar())
             : null,
         androidRuleIdeInfo.getHasIdlSources(),
         !Strings.isNullOrEmpty(androidRuleIdeInfo.getLegacyResources())
@@ -244,7 +235,6 @@
   }
 
   private static ProtoLibraryLegacyInfo makeProtoLibraryLegacyInfo(
-      ArtifactLocationDecoder decoder,
       AndroidStudioIdeInfo.ProtoLibraryLegacyJavaIdeInfo protoLibraryLegacyJavaIdeInfo) {
     final ProtoLibraryLegacyInfo.ApiFlavor apiFlavor;
     if (protoLibraryLegacyJavaIdeInfo.getApiVersion() == 1) {
@@ -267,9 +257,9 @@
     }
     return new ProtoLibraryLegacyInfo(
         apiFlavor,
-        makeLibraryArtifactList(decoder, protoLibraryLegacyJavaIdeInfo.getJars1List()),
-        makeLibraryArtifactList(decoder, protoLibraryLegacyJavaIdeInfo.getJarsMutableList()),
-        makeLibraryArtifactList(decoder, protoLibraryLegacyJavaIdeInfo.getJarsImmutableList()));
+        makeLibraryArtifactList(protoLibraryLegacyJavaIdeInfo.getJars1List()),
+        makeLibraryArtifactList(protoLibraryLegacyJavaIdeInfo.getJarsMutableList()),
+        makeLibraryArtifactList(protoLibraryLegacyJavaIdeInfo.getJarsImmutableList()));
   }
 
   private static JavaToolchainIdeInfo makeJavaToolchainIdeInfo(
@@ -279,10 +269,10 @@
   }
 
   private static Collection<LibraryArtifact> makeLibraryArtifactList(
-      ArtifactLocationDecoder decoder, List<AndroidStudioIdeInfo.LibraryArtifact> jarsList) {
+      List<AndroidStudioIdeInfo.LibraryArtifact> jarsList) {
     ImmutableList.Builder<LibraryArtifact> builder = ImmutableList.builder();
     for (AndroidStudioIdeInfo.LibraryArtifact libraryArtifact : jarsList) {
-      LibraryArtifact lib = makeLibraryArtifact(decoder, libraryArtifact);
+      LibraryArtifact lib = makeLibraryArtifact(libraryArtifact);
       if (lib != null) {
         builder.add(lib);
       }
@@ -292,16 +282,16 @@
 
   @Nullable
   private static LibraryArtifact makeLibraryArtifact(
-      ArtifactLocationDecoder decoder, AndroidStudioIdeInfo.LibraryArtifact libraryArtifact) {
+      AndroidStudioIdeInfo.LibraryArtifact libraryArtifact) {
     ArtifactLocation classJar =
-        libraryArtifact.hasJar() ? makeArtifactLocation(decoder, libraryArtifact.getJar()) : null;
+        libraryArtifact.hasJar() ? makeArtifactLocation(libraryArtifact.getJar()) : null;
     ArtifactLocation iJar =
         libraryArtifact.hasInterfaceJar()
-            ? makeArtifactLocation(decoder, libraryArtifact.getInterfaceJar())
+            ? makeArtifactLocation(libraryArtifact.getInterfaceJar())
             : null;
     ArtifactLocation sourceJar =
         libraryArtifact.hasSourceJar()
-            ? makeArtifactLocation(decoder, libraryArtifact.getSourceJar())
+            ? makeArtifactLocation(libraryArtifact.getSourceJar())
             : null;
     if (iJar == null && classJar == null) {
       // Failed to find ArtifactLocation file --
@@ -312,10 +302,10 @@
   }
 
   private static List<ArtifactLocation> makeArtifactLocationList(
-      ArtifactLocationDecoder decoder, List<AndroidStudioIdeInfo.ArtifactLocation> sourcesList) {
+      List<AndroidStudioIdeInfo.ArtifactLocation> sourcesList) {
     ImmutableList.Builder<ArtifactLocation> builder = ImmutableList.builder();
     for (AndroidStudioIdeInfo.ArtifactLocation pbArtifactLocation : sourcesList) {
-      ArtifactLocation loc = makeArtifactLocation(decoder, pbArtifactLocation);
+      ArtifactLocation loc = makeArtifactLocation(pbArtifactLocation);
       if (loc != null) {
         builder.add(loc);
       }
@@ -325,11 +315,15 @@
 
   @Nullable
   private static ArtifactLocation makeArtifactLocation(
-      ArtifactLocationDecoder decoder, AndroidStudioIdeInfo.ArtifactLocation pbArtifactLocation) {
+      AndroidStudioIdeInfo.ArtifactLocation pbArtifactLocation) {
     if (pbArtifactLocation == null) {
       return null;
     }
-    return decoder.decode(pbArtifactLocation);
+    return ArtifactLocation.builder()
+        .setRootExecutionPathFragment(pbArtifactLocation.getRootExecutionPathFragment())
+        .setRelativePath(pbArtifactLocation.getRelativePath())
+        .setIsSource(pbArtifactLocation.getIsSource())
+        .build();
   }
 
   private static Collection<Label> makeLabelListFromProtobuf(ProtocolStringList dependenciesList) {
diff --git a/base/src/com/google/idea/blaze/base/sync/status/BlazeSyncStatusListener.java b/base/src/com/google/idea/blaze/base/sync/status/BlazeSyncStatusListener.java
index 05ca578..e843332 100644
--- a/base/src/com/google/idea/blaze/base/sync/status/BlazeSyncStatusListener.java
+++ b/base/src/com/google/idea/blaze/base/sync/status/BlazeSyncStatusListener.java
@@ -15,6 +15,7 @@
  */
 package com.google.idea.blaze.base.sync.status;
 
+import com.google.idea.blaze.base.scope.BlazeContext;
 import com.google.idea.blaze.base.sync.SyncListener;
 import com.intellij.openapi.project.Project;
 
@@ -25,12 +26,12 @@
 public class BlazeSyncStatusListener extends SyncListener.Adapter {
 
   @Override
-  public void onSyncStart(Project project) {
+  public void onSyncStart(Project project, BlazeContext context) {
     BlazeSyncStatusImpl.getImpl(project).syncStarted();
   }
 
   @Override
-  public void afterSync(Project project, SyncResult syncResult) {
+  public void afterSync(Project project, BlazeContext context, SyncResult syncResult) {
     BlazeSyncStatusImpl.getImpl(project).syncEnded(syncResult);
   }
 }
diff --git a/base/src/com/google/idea/blaze/base/sync/workspace/ArtifactLocationDecoder.java b/base/src/com/google/idea/blaze/base/sync/workspace/ArtifactLocationDecoder.java
index 13fce58..ccdeb54 100644
--- a/base/src/com/google/idea/blaze/base/sync/workspace/ArtifactLocationDecoder.java
+++ b/base/src/com/google/idea/blaze/base/sync/workspace/ArtifactLocationDecoder.java
@@ -16,72 +16,18 @@
 package com.google.idea.blaze.base.sync.workspace;
 
 import com.google.idea.blaze.base.ideinfo.ArtifactLocation;
-import com.google.repackaged.devtools.build.lib.ideinfo.androidstudio.AndroidStudioIdeInfo;
-import com.google.repackaged.devtools.build.lib.ideinfo.androidstudio.PackageManifestOuterClass;
 import java.io.File;
-import javax.annotation.Nullable;
+import java.io.Serializable;
+import java.util.Collection;
+import java.util.List;
+import java.util.stream.Collectors;
 
 /** Decodes android_studio_ide_info.proto ArtifactLocation file paths */
-public class ArtifactLocationDecoder {
+public interface ArtifactLocationDecoder extends Serializable {
 
-  private final BlazeRoots blazeRoots;
-  private final WorkspacePathResolver pathResolver;
+  File decode(ArtifactLocation artifactLocation);
 
-  public ArtifactLocationDecoder(BlazeRoots blazeRoots, WorkspacePathResolver pathResolver) {
-    this.blazeRoots = blazeRoots;
-    this.pathResolver = pathResolver;
-  }
-
-  /**
-   * Decodes the ArtifactLocation proto, locates the absolute artifact file path. Returns null if
-   * the file can't be found (presumably because it was removed since the blaze build)
-   */
-  @Nullable
-  public ArtifactLocation decode(AndroidStudioIdeInfo.ArtifactLocation loc) {
-    return decode(
-        loc.getRootExecutionPathFragment(),
-        loc.getRelativePath(),
-        loc.getIsSource());
-  }
-
-  /**
-   * Decodes the ArtifactLocation proto, locates the absolute artifact file path. Returns null if
-   * the file can't be found (presumably because it was removed since the blaze build)
-   */
-  @Nullable
-  public ArtifactLocation decode(PackageManifestOuterClass.ArtifactLocation loc) {
-    return decode(
-        loc.getRootExecutionPathFragment(),
-        loc.getRelativePath(),
-        loc.getIsSource());
-  }
-
-  @Nullable
-  private ArtifactLocation decode(
-      String rootExecutionPathFragment, String relativePath, boolean isSource) {
-    File root;
-    if (isSource) {
-      root = pathResolver.findPackageRoot(relativePath);
-    } else {
-      root = new File(blazeRoots.executionRoot, rootExecutionPathFragment);
-    }
-    if (root == null) {
-      return null;
-    }
-    return ArtifactLocation.builder()
-        .setRootPath(root.toString())
-        .setRootExecutionPathFragment(rootExecutionPathFragment)
-        .setRelativePath(relativePath)
-        .setIsSource(isSource)
-        .build();
-  }
-
-  @Deprecated
-  private String deriveRootExecutionPathFragmentFromRoot(String rootPath) {
-    String execRoot = blazeRoots.executionRoot.toString();
-    if (rootPath.startsWith(execRoot)) {
-      return rootPath.substring(execRoot.length());
-    }
-    return "";
+  default List<File> decodeAll(Collection<ArtifactLocation> artifactLocations) {
+    return artifactLocations.stream().map(this::decode).collect(Collectors.toList());
   }
 }
diff --git a/base/src/com/google/idea/blaze/base/sync/workspace/ArtifactLocationDecoderImpl.java b/base/src/com/google/idea/blaze/base/sync/workspace/ArtifactLocationDecoderImpl.java
new file mode 100644
index 0000000..f31cace
--- /dev/null
+++ b/base/src/com/google/idea/blaze/base/sync/workspace/ArtifactLocationDecoderImpl.java
@@ -0,0 +1,42 @@
+/*
+ * Copyright 2016 The Bazel Authors. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.idea.blaze.base.sync.workspace;
+
+import com.google.idea.blaze.base.ideinfo.ArtifactLocation;
+import java.io.File;
+
+/** Decodes android_studio_ide_info.proto ArtifactLocation file paths */
+public class ArtifactLocationDecoderImpl implements ArtifactLocationDecoder {
+  private static final long serialVersionUID = 1L;
+
+  private final BlazeRoots blazeRoots;
+  private final WorkspacePathResolver pathResolver;
+
+  public ArtifactLocationDecoderImpl(BlazeRoots blazeRoots, WorkspacePathResolver pathResolver) {
+    this.blazeRoots = blazeRoots;
+    this.pathResolver = pathResolver;
+  }
+
+  @Override
+  public File decode(ArtifactLocation artifactLocation) {
+    if (artifactLocation.isSource) {
+      File root = pathResolver.findPackageRoot(artifactLocation.getRelativePath());
+      return new File(root, artifactLocation.getRelativePath());
+    } else {
+      return new File(blazeRoots.executionRoot, artifactLocation.getExecutionRootRelativePath());
+    }
+  }
+}
diff --git a/base/src/com/google/idea/blaze/base/sync/workspace/WorkspacePathResolver.java b/base/src/com/google/idea/blaze/base/sync/workspace/WorkspacePathResolver.java
index d01975d..f7f8df3 100644
--- a/base/src/com/google/idea/blaze/base/sync/workspace/WorkspacePathResolver.java
+++ b/base/src/com/google/idea/blaze/base/sync/workspace/WorkspacePathResolver.java
@@ -28,16 +28,14 @@
  */
 public interface WorkspacePathResolver extends Serializable {
   /** Resolves a workspace path to an absolute file. */
-  @Nullable
   default File resolveToFile(WorkspacePath workspacepath) {
     return resolveToFile(workspacepath.relativePath());
   }
 
   /** Resolves a workspace relative path to an absolute file. */
-  @Nullable
   default File resolveToFile(String workspaceRelativePath) {
     File packageRoot = findPackageRoot(workspaceRelativePath);
-    return packageRoot != null ? new File(packageRoot, workspaceRelativePath) : null;
+    return new File(packageRoot, workspaceRelativePath);
   }
 
   /**
@@ -47,7 +45,6 @@
   ImmutableList<File> resolveToIncludeDirectories(ExecutionRootPath executionRootPath);
 
   /** Finds the package root directory that a workspace relative path is in. */
-  @Nullable
   File findPackageRoot(String relativePath);
 
   /**
diff --git a/base/src/com/google/idea/blaze/base/sync/workspace/WorkspacePathResolverImpl.java b/base/src/com/google/idea/blaze/base/sync/workspace/WorkspacePathResolverImpl.java
index 5721b91..0dc1c69 100644
--- a/base/src/com/google/idea/blaze/base/sync/workspace/WorkspacePathResolverImpl.java
+++ b/base/src/com/google/idea/blaze/base/sync/workspace/WorkspacePathResolverImpl.java
@@ -51,7 +51,6 @@
   }
 
   @Override
-  @Nullable
   public File findPackageRoot(String relativePath) {
     if (packagePaths.size() == 1) {
       return packagePaths.get(0);
@@ -63,7 +62,9 @@
         return pkg;
       }
     }
-    return null;
+
+    // Return first in package path, even though it might not exist
+    return packagePaths.get(0);
   }
 
   @Nullable
diff --git a/base/src/com/google/idea/blaze/base/util/BlazeHelperBinaryUtil.java b/base/src/com/google/idea/blaze/base/util/BlazeHelperBinaryUtil.java
index d89004e..49fb501 100644
--- a/base/src/com/google/idea/blaze/base/util/BlazeHelperBinaryUtil.java
+++ b/base/src/com/google/idea/blaze/base/util/BlazeHelperBinaryUtil.java
@@ -58,6 +58,7 @@
     try (InputStream inputStream = URLUtil.openResourceStream(url)) {
       Files.copy(inputStream, file.toPath(), StandardCopyOption.REPLACE_EXISTING);
       file.setExecutable(true);
+      file.deleteOnExit();
       cachedFiles.put(binaryName, file);
       return file;
     } catch (IOException e) {
diff --git a/base/src/com/google/idea/blaze/base/vcs/VcsWorkspacePathResolver.java b/base/src/com/google/idea/blaze/base/vcs/VcsWorkspacePathResolver.java
deleted file mode 100644
index 2b5516c..0000000
--- a/base/src/com/google/idea/blaze/base/vcs/VcsWorkspacePathResolver.java
+++ /dev/null
@@ -1,26 +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.vcs;
-
-import java.io.File;
-import javax.annotation.Nullable;
-
-/** Created by tomlu on 5/13/16. */
-public interface VcsWorkspacePathResolver {
-
-  @Nullable
-  File findPackageRoot(String relativePath);
-}
diff --git a/base/tests/integrationtests/com/google/idea/blaze/base/lang/buildfile/completion/ArgumentCompletionContributorTest.java b/base/tests/integrationtests/com/google/idea/blaze/base/lang/buildfile/completion/ArgumentCompletionContributorTest.java
index db6e935..8db9661 100644
--- a/base/tests/integrationtests/com/google/idea/blaze/base/lang/buildfile/completion/ArgumentCompletionContributorTest.java
+++ b/base/tests/integrationtests/com/google/idea/blaze/base/lang/buildfile/completion/ArgumentCompletionContributorTest.java
@@ -22,67 +22,80 @@
 import com.intellij.codeInsight.lookup.LookupElement;
 import com.intellij.openapi.editor.Editor;
 import com.intellij.testFramework.fixtures.CompletionAutoPopupTester;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
 
 /** Tests for code completion of funcall arguments. */
+@RunWith(JUnit4.class)
 public class ArgumentCompletionContributorTest extends BuildFileIntegrationTestCase {
 
   private CompletionAutoPopupTester completionTester;
 
-  public void doSetup() {
-    super.doSetup();
+  @Before
+  public final void before() {
     completionTester = new CompletionAutoPopupTester(testFixture);
   }
 
+  /** Completion UI testing can't be run on the EDT. */
   @Override
-  protected boolean runInDispatchThread() {
+  protected boolean runTestsOnEdt() {
     return false;
   }
 
-  @Override
-  protected void invokeTestRunnable(Runnable runnable) throws Exception {
-    completionTester.runWithAutoPopupEnabled(runnable);
-  }
-
+  @Test
   public void testIncompleteFuncall() {
-    BuildFile file =
-        createBuildFile(
-            "BUILD", "def function(name, deps, srcs):", "  # empty function", "function(d");
+    completionTester.runWithAutoPopupEnabled(
+        () -> {
+          BuildFile file =
+              createBuildFile(
+                  "BUILD", "def function(name, deps, srcs):", "  # empty function", "function(d");
 
-    Editor editor = openFileInEditor(file.getVirtualFile());
-    setCaretPosition(editor, 2, "function(n".length());
+          Editor editor = openFileInEditor(file.getVirtualFile());
+          setCaretPosition(editor, 2, "function(n".length());
 
-    LookupElement[] completionItems = testFixture.completeBasic();
-    assertThat(completionItems).isNull();
+          LookupElement[] completionItems = testFixture.completeBasic();
+          assertThat(completionItems).isNull();
 
-    assertFileContents(
-        file, "def function(name, deps, srcs):", "  # empty function", "function(deps");
+          assertFileContents(
+              file, "def function(name, deps, srcs):", "  # empty function", "function(deps");
+        });
   }
 
+  @Test
   public void testExistingKeywordArg() {
-    BuildFile file =
-        createBuildFile(
-            "BUILD",
-            "def function(name, deps, srcs):",
-            "  # empty function",
-            "function(name = \"lib\")");
+    completionTester.runWithAutoPopupEnabled(
+        () -> {
+          BuildFile file =
+              createBuildFile(
+                  "BUILD",
+                  "def function(name, deps, srcs):",
+                  "  # empty function",
+                  "function(name = \"lib\")");
 
-    Editor editor = openFileInEditor(file.getVirtualFile());
-    setCaretPosition(editor, 2, "function(".length());
+          Editor editor = openFileInEditor(file.getVirtualFile());
+          setCaretPosition(editor, 2, "function(".length());
 
-    String[] completionItems = getCompletionItemsAsStrings();
-    assertThat(completionItems).hasLength(4);
-    assertThat(completionItems).asList().containsAllOf("name", "deps", "srcs", "function");
+          String[] completionItems = getCompletionItemsAsStrings();
+          assertThat(completionItems).hasLength(4);
+          assertThat(completionItems).asList().containsAllOf("name", "deps", "srcs", "function");
+        });
   }
 
+  @Test
   public void testNoArgumentCompletionInComment() {
-    BuildFile file =
-        createBuildFile(
-            "BUILD", "def function(name, deps, srcs):", "  # empty function", "function(#");
+    completionTester.runWithAutoPopupEnabled(
+        () -> {
+          BuildFile file =
+              createBuildFile(
+                  "BUILD", "def function(name, deps, srcs):", "  # empty function", "function(#");
 
-    Editor editor = openFileInEditor(file.getVirtualFile());
-    setCaretPosition(editor, 2, "function(#".length());
+          Editor editor = openFileInEditor(file.getVirtualFile());
+          setCaretPosition(editor, 2, "function(#".length());
 
-    completionTester.typeWithPauses("n");
-    assertNull(testFixture.getLookup());
+          completionTester.typeWithPauses("n");
+          assertThat(testFixture.getLookup()).isNull();
+        });
   }
 }
diff --git a/base/tests/integrationtests/com/google/idea/blaze/base/lang/buildfile/completion/BuiltInFunctionAttributeCompletionContributorTest.java b/base/tests/integrationtests/com/google/idea/blaze/base/lang/buildfile/completion/BuiltInFunctionAttributeCompletionContributorTest.java
index 6fe0d21..dba6e54 100644
--- a/base/tests/integrationtests/com/google/idea/blaze/base/lang/buildfile/completion/BuiltInFunctionAttributeCompletionContributorTest.java
+++ b/base/tests/integrationtests/com/google/idea/blaze/base/lang/buildfile/completion/BuiltInFunctionAttributeCompletionContributorTest.java
@@ -29,20 +29,25 @@
 import com.intellij.openapi.editor.Editor;
 import com.intellij.openapi.project.Project;
 import javax.annotation.Nullable;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
 
 /** Tests for BuiltInFunctionAttributeCompletionContributor. */
+@RunWith(JUnit4.class)
 public class BuiltInFunctionAttributeCompletionContributorTest
     extends BuildFileIntegrationTestCase {
 
   private MockBuildLanguageSpecProvider specProvider;
 
-  @Override
-  protected void doSetup() {
-    super.doSetup();
+  @Before
+  public final void before() {
     specProvider = new MockBuildLanguageSpecProvider();
     registerApplicationService(BuildLanguageSpecProvider.class, specProvider);
   }
 
+  @Test
   public void testSimpleCompletion() {
     setRuleAndAttributes("sh_binary", "name", "deps", "srcs", "data");
 
@@ -55,6 +60,7 @@
     assertThat(completionItems).asList().containsAllOf("name", "deps", "srcs", "data");
   }
 
+  @Test
   public void testSimpleSingleCompletion() {
     setRuleAndAttributes("sh_binary", "name", "deps", "srcs", "data");
 
@@ -68,6 +74,7 @@
     assertFileContents(file, "sh_binary(", "    name");
   }
 
+  @Test
   public void testNoCompletionInUnknownRule() {
     setRuleAndAttributes("sh_binary", "name", "deps", "srcs", "data");
 
@@ -80,6 +87,7 @@
     assertThat(completionItems).isEmpty();
   }
 
+  @Test
   public void testNoCompletionInComment() {
     setRuleAndAttributes("sh_binary", "name", "deps", "srcs", "data");
 
@@ -87,9 +95,10 @@
 
     Editor editor = openFileInEditor(file.getVirtualFile());
     setCaretPosition(editor, 0, "sh_binary(#".length());
-    assertEmpty(getCompletionItemsAsStrings());
+    assertThat(getCompletionItemsAsStrings()).isEmpty();
   }
 
+  @Test
   public void testCompletionInSkylarkExtension() {
     setRuleAndAttributes("sh_binary", "name", "deps", "srcs", "data");
 
diff --git a/base/tests/integrationtests/com/google/idea/blaze/base/lang/buildfile/completion/BuiltInFunctionCompletionContributorTest.java b/base/tests/integrationtests/com/google/idea/blaze/base/lang/buildfile/completion/BuiltInFunctionCompletionContributorTest.java
index f448b7a..eb36aa4 100644
--- a/base/tests/integrationtests/com/google/idea/blaze/base/lang/buildfile/completion/BuiltInFunctionCompletionContributorTest.java
+++ b/base/tests/integrationtests/com/google/idea/blaze/base/lang/buildfile/completion/BuiltInFunctionCompletionContributorTest.java
@@ -27,19 +27,24 @@
 import com.intellij.openapi.editor.Editor;
 import com.intellij.openapi.project.Project;
 import javax.annotation.Nullable;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
 
 /** Tests BuiltInFunctionCompletionContributor */
+@RunWith(JUnit4.class)
 public class BuiltInFunctionCompletionContributorTest extends BuildFileIntegrationTestCase {
 
   private MockBuildLanguageSpecProvider specProvider;
 
-  @Override
-  protected void doSetup() {
-    super.doSetup();
+  @Before
+  public final void before() {
     specProvider = new MockBuildLanguageSpecProvider();
     registerApplicationService(BuildLanguageSpecProvider.class, specProvider);
   }
 
+  @Test
   public void testSimpleTopLevelCompletion() {
     setRules("java_library", "android_binary");
 
@@ -56,6 +61,7 @@
     assertFileContents(file, "");
   }
 
+  @Test
   public void testUniqueTopLevelCompletion() {
     setRules("java_library", "android_binary");
 
@@ -71,6 +77,7 @@
     assertCaretPosition(editor, 0, "java_library(".length());
   }
 
+  @Test
   public void testSkylarkNativeCompletion() {
     setRules("java_library", "android_binary");
 
@@ -86,6 +93,7 @@
     assertCaretPosition(editor, 1, "  native.java_library(".length());
   }
 
+  @Test
   public void testNoCompletionInsideRule() {
     setRules("java_library", "android_binary");
 
@@ -101,6 +109,7 @@
     assertFileContents(file, contents);
   }
 
+  @Test
   public void testNoCompletionInComment() {
     setRules("java_library", "android_binary");
 
@@ -109,7 +118,7 @@
     Editor editor = openFileInEditor(file.getVirtualFile());
     setCaretPosition(editor, 0, "#java".length());
 
-    assertEmpty(getCompletionItemsAsStrings());
+    assertThat(getCompletionItemsAsStrings()).isEmpty();
   }
 
   private void setRules(String... ruleNames) {
diff --git a/base/tests/integrationtests/com/google/idea/blaze/base/lang/buildfile/completion/FilePathCompletionTest.java b/base/tests/integrationtests/com/google/idea/blaze/base/lang/buildfile/completion/FilePathCompletionTest.java
index 5838f8f..a6024e5 100644
--- a/base/tests/integrationtests/com/google/idea/blaze/base/lang/buildfile/completion/FilePathCompletionTest.java
+++ b/base/tests/integrationtests/com/google/idea/blaze/base/lang/buildfile/completion/FilePathCompletionTest.java
@@ -21,10 +21,15 @@
 import com.google.idea.blaze.base.lang.buildfile.psi.BuildFile;
 import com.intellij.openapi.editor.Editor;
 import com.intellij.openapi.vfs.VirtualFile;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
 
 /** Tests file path code completion in BUILD file labels. */
+@RunWith(JUnit4.class)
 public class FilePathCompletionTest extends BuildFileIntegrationTestCase {
 
+  @Test
   public void testUniqueDirectoryCompleted() {
     BuildFile file = createBuildFile("java/BUILD", "'//'");
 
@@ -37,6 +42,7 @@
     assertCaretPosition(editor, 0, "'//java".length());
   }
 
+  @Test
   public void testUniqueMultiSegmentDirectoryCompleted() {
     BuildFile file = createBuildFile("java/com/google/BUILD", "'//'");
 
@@ -50,6 +56,7 @@
   // expected to be a typical workflow -- complete a segment,
   // get the possibilities, then start typing
   // next segment and complete again
+  @Test
   public void testMultiStageCompletion() {
     createDirectory("foo");
     createDirectory("bar");
@@ -77,6 +84,7 @@
     assertCaretPosition(editor, 0, "'//other/foo".length());
   }
 
+  @Test
   public void testCompletionSuggestionString() {
     createDirectory("foo");
     createDirectory("bar");
diff --git a/base/tests/integrationtests/com/google/idea/blaze/base/lang/buildfile/completion/LocalSymbolCompletionTest.java b/base/tests/integrationtests/com/google/idea/blaze/base/lang/buildfile/completion/LocalSymbolCompletionTest.java
index aab1659..1381c31 100644
--- a/base/tests/integrationtests/com/google/idea/blaze/base/lang/buildfile/completion/LocalSymbolCompletionTest.java
+++ b/base/tests/integrationtests/com/google/idea/blaze/base/lang/buildfile/completion/LocalSymbolCompletionTest.java
@@ -20,8 +20,12 @@
 import com.google.common.base.Joiner;
 import com.google.idea.blaze.base.lang.buildfile.BuildFileIntegrationTestCase;
 import com.intellij.psi.PsiFile;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
 
 /** Tests code completion works with general symbols in scope. */
+@RunWith(JUnit4.class)
 public class LocalSymbolCompletionTest extends BuildFileIntegrationTestCase {
 
   private PsiFile setInput(String... fileContents) {
@@ -29,10 +33,11 @@
   }
 
   private void assertResult(String... resultingFileContents) {
-    String s = testFixture.getFile().getText();
+    testFixture.getFile().getText();
     testFixture.checkResult(Joiner.on("\n").join(resultingFileContents));
   }
 
+  @Test
   public void testLocalVariable() {
     setInput("var = [a, b]", "def function(name, deps, srcs):", "  v<caret>");
 
@@ -41,6 +46,7 @@
     assertResult("var = [a, b]", "def function(name, deps, srcs):", "  var<caret>");
   }
 
+  @Test
   public void testLocalFunction() {
     setInput("def fnName():return True", "def function(name, deps, srcs):", "  fnN<caret>");
 
@@ -49,6 +55,7 @@
     assertResult("def fnName():return True", "def function(name, deps, srcs):", "  fnName<caret>");
   }
 
+  @Test
   public void testNoCompletionAfterDot() {
     setInput("var = [a, b]", "def function(name, deps, srcs):", "  ext.v<caret>");
 
@@ -56,6 +63,7 @@
     assertThat(completionItems).isEmpty();
   }
 
+  @Test
   public void testFunctionParam() {
     setInput("def test(var):", "  v<caret>");
 
@@ -66,6 +74,7 @@
 
   // b/28912523: when symbol is present in multiple assignment statements, should only be
   // included once in the code-completion dialog
+  @Test
   public void testSymbolAssignedMultipleTimes() {
     setInput("var = 1", "var = 2", "var = 3", "<caret>");
 
@@ -74,18 +83,21 @@
     assertResult("var = 1", "var = 2", "var = 3", "var<caret>");
   }
 
+  @Test
   public void testSymbolDefinedOutsideScope() {
     setInput("<caret>", "var = 1");
 
     assertThat(getCompletionItemsAsStrings()).isEmpty();
   }
 
+  @Test
   public void testSymbolDefinedOutsideScope2() {
     setInput("def fn():", "  var = 1", "v<caret>");
 
     assertThat(testFixture.completeBasic()).isEmpty();
   }
 
+  @Test
   public void testSymbolDefinedOutsideScope3() {
     setInput("for var in (1, 2, 3): print var", "v<caret>");
 
diff --git a/base/tests/integrationtests/com/google/idea/blaze/base/lang/buildfile/completion/ParameterCompletionContributorTest.java b/base/tests/integrationtests/com/google/idea/blaze/base/lang/buildfile/completion/ParameterCompletionContributorTest.java
index afc25d0..4989580 100644
--- a/base/tests/integrationtests/com/google/idea/blaze/base/lang/buildfile/completion/ParameterCompletionContributorTest.java
+++ b/base/tests/integrationtests/com/google/idea/blaze/base/lang/buildfile/completion/ParameterCompletionContributorTest.java
@@ -21,10 +21,15 @@
 import com.google.idea.blaze.base.lang.buildfile.psi.BuildFile;
 import com.intellij.codeInsight.lookup.LookupElement;
 import com.intellij.openapi.editor.Editor;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
 
 /** Tests ParameterCompletionContributor. */
+@RunWith(JUnit4.class)
 public class ParameterCompletionContributorTest extends BuildFileIntegrationTestCase {
 
+  @Test
   public void testArgsCompletion() {
     BuildFile file = createBuildFile("BUILD", "def function(arg1, *");
 
@@ -37,6 +42,7 @@
     assertFileContents(file, "def function(arg1, *args");
   }
 
+  @Test
   public void testKwargsCompletion() {
     BuildFile file = createBuildFile("BUILD", "def function(arg1, **");
 
diff --git a/base/tests/integrationtests/com/google/idea/blaze/base/lang/buildfile/completion/RuleTargetCompletionTest.java b/base/tests/integrationtests/com/google/idea/blaze/base/lang/buildfile/completion/RuleTargetCompletionTest.java
index 6a289f7..2df632e 100644
--- a/base/tests/integrationtests/com/google/idea/blaze/base/lang/buildfile/completion/RuleTargetCompletionTest.java
+++ b/base/tests/integrationtests/com/google/idea/blaze/base/lang/buildfile/completion/RuleTargetCompletionTest.java
@@ -21,10 +21,15 @@
 import com.google.idea.blaze.base.lang.buildfile.psi.BuildFile;
 import com.intellij.codeInsight.lookup.LookupElement;
 import com.intellij.openapi.editor.Editor;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
 
 /** Tests code completion of rule target labels. */
+@RunWith(JUnit4.class)
 public class RuleTargetCompletionTest extends BuildFileIntegrationTestCase {
 
+  @Test
   public void testLocalTarget() {
     BuildFile file =
         createBuildFile(
@@ -42,6 +47,7 @@
     assertThat(completionItems[0].toString()).isEqualTo("':lib'");
   }
 
+  @Test
   public void testIgnoreContainingTarget() {
     BuildFile file =
         createBuildFile(
@@ -54,6 +60,7 @@
     assertThat(completionItems).isEmpty();
   }
 
+  @Test
   public void testNotCodeCompletionInNameField() {
     BuildFile file =
         createBuildFile(
@@ -70,8 +77,9 @@
     assertThat(completionItems).isEmpty();
   }
 
+  @Test
   public void testNonLocalTarget() {
-    BuildFile foo = createBuildFile("java/com/google/foo/BUILD", "java_library(name = 'foo_lib')");
+    createBuildFile("java/com/google/foo/BUILD", "java_library(name = 'foo_lib')");
 
     BuildFile bar =
         createBuildFile(
@@ -87,8 +95,9 @@
     assertThat(completionItems).asList().containsExactly("'//java/com/google/foo:foo_lib'");
   }
 
+  @Test
   public void testNonLocalRulesNotCompletedWithoutColon() {
-    BuildFile foo = createBuildFile("java/com/google/foo/BUILD", "java_library(name = 'foo_lib')");
+    createBuildFile("java/com/google/foo/BUILD", "java_library(name = 'foo_lib')");
 
     BuildFile bar =
         createBuildFile(
@@ -104,6 +113,7 @@
     assertThat(completionItems).isEmpty();
   }
 
+  @Test
   public void testPackageLocalRulesCompletedWithoutColon() {
     BuildFile file =
         createBuildFile(
@@ -125,8 +135,9 @@
         "    deps = ['lib']");
   }
 
+  @Test
   public void testLocalPathIgnoredForNonLocalLabels() {
-    BuildFile rootPackage = createBuildFile("java/BUILD", "java_library(name = 'root_rule')");
+    createBuildFile("java/BUILD", "java_library(name = 'root_rule')");
 
     BuildFile otherPackage =
         createBuildFile(
diff --git a/base/tests/integrationtests/com/google/idea/blaze/base/lang/buildfile/completion/SkylarkExtensionCompletionTest.java b/base/tests/integrationtests/com/google/idea/blaze/base/lang/buildfile/completion/SkylarkExtensionCompletionTest.java
index 9038c9b..f402723 100644
--- a/base/tests/integrationtests/com/google/idea/blaze/base/lang/buildfile/completion/SkylarkExtensionCompletionTest.java
+++ b/base/tests/integrationtests/com/google/idea/blaze/base/lang/buildfile/completion/SkylarkExtensionCompletionTest.java
@@ -19,8 +19,12 @@
 
 import com.google.idea.blaze.base.lang.buildfile.BuildFileIntegrationTestCase;
 import com.intellij.openapi.vfs.VirtualFile;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
 
 /** Tests auto-complete of skylark bzl files in 'load' statements. */
+@RunWith(JUnit4.class)
 public class SkylarkExtensionCompletionTest extends BuildFileIntegrationTestCase {
 
   private VirtualFile createAndSetCaret(String filePath, String... fileContents) {
@@ -29,6 +33,7 @@
     return file;
   }
 
+  @Test
   public void testSimpleCase() {
     createFile("skylark.bzl");
     VirtualFile file = createAndSetCaret("BUILD", "load(':<caret>'");
@@ -37,13 +42,15 @@
     assertFileContents(file, "load(':skylark.bzl'");
   }
 
+  @Test
   public void testSelfNotInResults() {
     createFile("BUILD");
-    VirtualFile file = createAndSetCaret("self.bzl", "load(':<caret>'");
+    createAndSetCaret("self.bzl", "load(':<caret>'");
 
     assertThat(testFixture.completeBasic()).isEmpty();
   }
 
+  @Test
   public void testSelfNotInResults2() {
     createFile("skylark.bzl");
     createFile("BUILD");
@@ -53,6 +60,7 @@
     assertFileContents(file, "load(':skylark.bzl'");
   }
 
+  @Test
   public void testNoRulesInResults() {
     createFile("java/com/google/foo/skylark.bzl");
     createFile("java/com/google/foo/BUILD", "java_library(name = 'foo')");
@@ -69,15 +77,16 @@
     assertFileContents(file, "'//java/com/google/foo:foo'");
   }
 
+  @Test
   public void testNonSkylarkFilesNotInResults() {
     createFile("java/com/google/foo/text.txt");
 
-    VirtualFile file =
-        createAndSetCaret("java/com/google/bar/BUILD", "load('//java/com/google/foo:<caret>'");
+    createAndSetCaret("java/com/google/bar/BUILD", "load('//java/com/google/foo:<caret>'");
 
     assertThat(testFixture.completeBasic()).isEmpty();
   }
 
+  @Test
   public void testLabelStartsWithColon() {
     createFile("java/com/google/skylark.bzl");
     VirtualFile file = createAndSetCaret("java/com/google/BUILD", "load(':<caret>'");
@@ -86,6 +95,7 @@
     assertFileContents(file, "load(':skylark.bzl'");
   }
 
+  @Test
   public void testLabelStartsWithSlashes() {
     createFile("java/com/google/skylark.bzl");
     VirtualFile file =
@@ -95,6 +105,7 @@
     assertFileContents(file, "load('//java/com/google:skylark.bzl'");
   }
 
+  @Test
   public void testLabelStartsWithSlashesWithoutColon() {
     createFile("java/com/google/skylark.bzl");
     VirtualFile file =
@@ -104,6 +115,7 @@
     assertFileContents(file, "load('//java/com/google:skylark.bzl'");
   }
 
+  @Test
   public void testDirectoryCompletionInLoadStatement() {
     createFile("java/com/google/skylark.bzl");
     VirtualFile file = createAndSetCaret("java/com/google/BUILD", "load('//<caret>'");
@@ -115,11 +127,11 @@
     assertFileContents(file, "load('//java/com/google:skylark.bzl'");
   }
 
+  @Test
   public void testMultipleFiles() {
     createFile("java/com/google/skylark.bzl");
     createFile("java/com/google/other.bzl");
-    VirtualFile file =
-        createAndSetCaret("java/com/google/BUILD", "load('//java/com/google:<caret>'");
+    createAndSetCaret("java/com/google/BUILD", "load('//java/com/google:<caret>'");
 
     String[] strings = getCompletionItemsAsStrings();
     assertThat(strings).hasLength(2);
@@ -130,6 +142,7 @@
 
   // relative paths in skylark extensions which lie in subdirectories
   // are relative to the parent blaze package directory
+  @Test
   public void testRelativePathInSubdirectory() {
     createFile("java/com/google/BUILD");
     createFile("java/com/google/nonPackageSubdirectory/skylark.bzl", "def function(): return");
diff --git a/base/tests/integrationtests/com/google/idea/blaze/base/lang/buildfile/completion/SkylarkExtensionSymbolCompletionTest.java b/base/tests/integrationtests/com/google/idea/blaze/base/lang/buildfile/completion/SkylarkExtensionSymbolCompletionTest.java
index 4c00b51..4f85d97 100644
--- a/base/tests/integrationtests/com/google/idea/blaze/base/lang/buildfile/completion/SkylarkExtensionSymbolCompletionTest.java
+++ b/base/tests/integrationtests/com/google/idea/blaze/base/lang/buildfile/completion/SkylarkExtensionSymbolCompletionTest.java
@@ -19,8 +19,12 @@
 
 import com.google.idea.blaze.base.lang.buildfile.BuildFileIntegrationTestCase;
 import com.intellij.openapi.vfs.VirtualFile;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
 
 /** Tests auto-complete of symbols loaded from skylark bzl files. */
+@RunWith(JUnit4.class)
 public class SkylarkExtensionSymbolCompletionTest extends BuildFileIntegrationTestCase {
 
   private VirtualFile createAndSetCaret(String filePath, String... fileContents) {
@@ -29,6 +33,7 @@
     return file;
   }
 
+  @Test
   public void testGlobalVariable() {
     createFile("skylark.bzl", "VAR = []");
     VirtualFile file = createAndSetCaret("BUILD", "load(':skylark.bzl', '<caret>')");
@@ -37,6 +42,7 @@
     assertFileContents(file, "load(':skylark.bzl', 'VAR')");
   }
 
+  @Test
   public void testFunctionStatement() {
     createFile("skylark.bzl", "def fn(param):stmt");
     VirtualFile file = createAndSetCaret("BUILD", "load(':skylark.bzl', '<caret>')");
@@ -45,22 +51,24 @@
     assertFileContents(file, "load(':skylark.bzl', 'fn')");
   }
 
+  @Test
   public void testMultipleOptions() {
     createFile("skylark.bzl", "def fn(param):stmt", "VAR = []");
-    VirtualFile file = createAndSetCaret("BUILD", "load(':skylark.bzl', '<caret>')");
+    createAndSetCaret("BUILD", "load(':skylark.bzl', '<caret>')");
 
     String[] options = getCompletionItemsAsStrings();
     assertThat(options).asList().containsExactly("'fn'", "'VAR'");
   }
 
+  @Test
   public void testRulesNotIncluded() {
     createFile("skylark.bzl", "java_library(name = 'lib')", "native.java_library(name = 'foo'");
-
-    VirtualFile file = createAndSetCaret("BUILD", "load(':skylark.bzl', '<caret>')");
+    createAndSetCaret("BUILD", "load(':skylark.bzl', '<caret>')");
 
     assertThat(testFixture.completeBasic()).isEmpty();
   }
 
+  @Test
   public void testLoadedSymbols() {
     createFile("other.bzl", "def function()");
     createFile("skylark.bzl", "load(':other.bzl', 'function')");
@@ -70,6 +78,7 @@
     assertFileContents(file, "load(':skylark.bzl', 'function')");
   }
 
+  @Test
   public void testNotLoadedSymbolsAreNotIncluded() {
     createFile("other.bzl", "def function():stmt", "def other_function():stmt");
     createFile("skylark.bzl", "load(':other.bzl', 'function')");
diff --git a/base/tests/integrationtests/com/google/idea/blaze/base/lang/buildfile/editor/BuildBraceMatcherTest.java b/base/tests/integrationtests/com/google/idea/blaze/base/lang/buildfile/editor/BuildBraceMatcherTest.java
index ac3b331..c4af1d4 100644
--- a/base/tests/integrationtests/com/google/idea/blaze/base/lang/buildfile/editor/BuildBraceMatcherTest.java
+++ b/base/tests/integrationtests/com/google/idea/blaze/base/lang/buildfile/editor/BuildBraceMatcherTest.java
@@ -18,14 +18,19 @@
 import com.google.common.base.Joiner;
 import com.google.idea.blaze.base.lang.buildfile.BuildFileIntegrationTestCase;
 import com.intellij.psi.PsiFile;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
 
 /** Test brace matching (auto-inserting closing braces when appropriate) */
+@RunWith(JUnit4.class)
 public class BuildBraceMatcherTest extends BuildFileIntegrationTestCase {
 
   private PsiFile setInput(String... fileContents) {
     return testFixture.configureByText("BUILD", Joiner.on("\n").join(fileContents));
   }
 
+  @Test
   public void testClosingParenInserted() {
     PsiFile file = setInput("java_library<caret>");
 
@@ -34,6 +39,7 @@
     assertFileContents(file, "java_library()");
   }
 
+  @Test
   public void testClosingBraceInserted() {
     PsiFile file = setInput("<caret>");
 
@@ -42,6 +48,7 @@
     assertFileContents(file, "{}");
   }
 
+  @Test
   public void testClosingBracketInserted() {
     PsiFile file = setInput("<caret>");
 
@@ -50,6 +57,7 @@
     assertFileContents(file, "[]");
   }
 
+  @Test
   public void testNoClosingBracketInsertedIfLaterDanglingRBracket() {
     PsiFile file = setInput("java_library(", "    srcs =<caret> 'source.java']", ")");
 
@@ -58,6 +66,7 @@
     assertFileContents(file, "java_library(", "    srcs =[ 'source.java']", ")");
   }
 
+  @Test
   public void testClosingBracketInsertedIfFollowedByWhitespace() {
     PsiFile file = setInput("java_library(", "    srcs =<caret> 'source.java'", ")");
 
@@ -66,6 +75,7 @@
     assertFileContents(file, "java_library(", "    srcs =[] 'source.java'", ")");
   }
 
+  @Test
   public void testNoClosingBraceInsertedWhenFollowedByIdentifier() {
     PsiFile file = setInput("hello = <caret>test");
 
diff --git a/base/tests/integrationtests/com/google/idea/blaze/base/lang/buildfile/editor/BuildIndentOnEnterTest.java b/base/tests/integrationtests/com/google/idea/blaze/base/lang/buildfile/editor/BuildIndentOnEnterTest.java
index eaccda5..684255a 100644
--- a/base/tests/integrationtests/com/google/idea/blaze/base/lang/buildfile/editor/BuildIndentOnEnterTest.java
+++ b/base/tests/integrationtests/com/google/idea/blaze/base/lang/buildfile/editor/BuildIndentOnEnterTest.java
@@ -18,8 +18,12 @@
 import com.google.common.base.Joiner;
 import com.google.idea.blaze.base.lang.buildfile.BuildFileIntegrationTestCase;
 import com.intellij.openapi.actionSystem.IdeActions;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
 
 /** Tests that indents are inserted correctly when enter is pressed. */
+@RunWith(JUnit4.class)
 public class BuildIndentOnEnterTest extends BuildFileIntegrationTestCase {
 
   private void setInput(String... fileContents) {
@@ -28,197 +32,235 @@
 
   private void pressEnterAndAssertResult(String... resultingFileContents) {
     pressButton(IdeActions.ACTION_EDITOR_ENTER);
-    String s = testFixture.getFile().getText();
+    testFixture.getFile().getText();
     testFixture.checkResult(Joiner.on("\n").join(resultingFileContents));
   }
 
+  @Test
   public void testSimpleIndent() {
     setInput("a=1<caret>");
     pressEnterAndAssertResult("a=1", "<caret>");
   }
 
+  @Test
   public void testAlignInListMiddle() {
     setInput("target = [a,<caret>", "          c]");
     pressEnterAndAssertResult("target = [a,", "          <caret>", "          c]");
   }
 
+  @Test
   public void testNoAlignAfterList() {
     setInput("target = [", "    arg", "]<caret>");
     pressEnterAndAssertResult("target = [", "    arg", "]", "<caret>");
   }
 
+  @Test
   public void testAlignInDict() {
     setInput("some_call({'aaa': 'v1',<caret>})");
     pressEnterAndAssertResult("some_call({'aaa': 'v1',", "           <caret>})");
   }
 
+  @Test
   public void testAlignInDictInParams() { // PY-1947
     setInput("foobar({<caret>})");
     pressEnterAndAssertResult("foobar({", "    <caret>", "})");
   }
 
+  @Test
   public void testAlignInEmptyList() {
     setInput("target = [<caret>]");
     pressEnterAndAssertResult("target = [", "    <caret>", "]");
   }
 
+  @Test
   public void testAlignInEmptyParens() {
     setInput("foo(<caret>)");
     pressEnterAndAssertResult("foo(", "    <caret>", ")");
   }
 
+  @Test
   public void testAlignInEmptyDict() {
     setInput("{<caret>}");
     pressEnterAndAssertResult("{", "    <caret>", "}");
   }
 
+  @Test
   public void testAlignInEmptyTuple() {
     setInput("(<caret>)");
     pressEnterAndAssertResult("(", "    <caret>", ")");
   }
 
+  @Test
   public void testEnterInNonEmptyArgList() {
     setInput("func(<caret>params=1)");
     pressEnterAndAssertResult("func(", "    <caret>params=1)");
   }
 
+  @Test
   public void testNoIndentAfterTuple() {
     setInput("()<caret>");
     pressEnterAndAssertResult("()", "<caret>");
   }
 
+  @Test
   public void testNoIndentAfterList() {
     setInput("target = [1, 2]<caret>");
     pressEnterAndAssertResult("target = [1, 2]", "<caret>");
   }
 
+  @Test
   public void testNoIndentAfterDict() {
     setInput("target = {}<caret>");
     pressEnterAndAssertResult("target = {}", "<caret>");
   }
 
+  @Test
   public void testEmptyFuncallStart() {
     setInput("func(<caret>", ")");
     pressEnterAndAssertResult("func(", "    <caret>", ")");
   }
 
+  @Test
   public void testEmptyFuncallAfterNewlineNoIndent() {
     setInput("func(", "<caret>)");
     pressEnterAndAssertResult("func(", "", "<caret>)");
   }
 
+  @Test
   public void testEmptyFuncallAfterNewlineWithIndent() {
     setInput("func(", "    <caret>", ")");
     pressEnterAndAssertResult("func(", "    ", "    <caret>", ")");
   }
 
+  @Test
   public void testFuncallAfterFirstArg() {
     setInput("func(", "    arg1,<caret>", ")");
     pressEnterAndAssertResult("func(", "    arg1,", "    <caret>", ")");
   }
 
+  @Test
   public void testFuncallFirstArgOnSameLine() {
     setInput("func(arg1, arg2,<caret>");
     pressEnterAndAssertResult("func(arg1, arg2,", "     <caret>");
   }
 
+  @Test
   public void testFuncallFirstArgOnSameLineWithClosingBrace() {
     setInput("func(arg1, arg2,<caret>)");
     pressEnterAndAssertResult("func(arg1, arg2,", "     <caret>)");
   }
 
+  @Test
   public void testNonEmptyDict() {
     setInput("{key1 : value1,<caret>}");
     pressEnterAndAssertResult("{key1 : value1,", " <caret>}");
   }
 
+  @Test
   public void testNonEmptyDictFirstArgIndented() {
     setInput("{", "    key1 : value1,<caret>" + "}");
     pressEnterAndAssertResult("{", "    key1 : value1,", "    <caret>" + "}");
   }
 
+  @Test
   public void testEmptyDictAlreadyIndented() {
     setInput("{", "    <caret>" + "}");
     pressEnterAndAssertResult("{", "    ", "    <caret>" + "}");
   }
 
+  @Test
   public void testEmptyParamIndent() {
     setInput("def fn(<caret>)");
     pressEnterAndAssertResult("def fn(", "    <caret>", ")");
   }
 
+  @Test
   public void testNonEmptyParamIndent() {
     setInput("def fn(param1,<caret>)");
     pressEnterAndAssertResult("def fn(param1,", "       <caret>)");
   }
 
+  @Test
   public void testFunctionDefAfterColon() {
     setInput("def fn():<caret>");
     pressEnterAndAssertResult("def fn():", "  <caret>");
   }
 
   // def fn():stmt* (THIS IS CURRENTLY BROKEN -- shouldn't indent but does)
+  @Test
   public void testFunctionDefSingleStatement() {
     setInput("def fn():stmt<caret>");
     pressEnterAndAssertResult("def fn():stmt", "<caret>");
   }
 
+  @Test
   public void testFunctionDefAfterFirstSuiteStatement() {
     setInput("def fn():", "  stmt1<caret>");
     pressEnterAndAssertResult("def fn():", "  stmt1", "  <caret>");
   }
 
+  @Test
   public void testNoIndentAfterSuiteDedentOnEmptyLine() {
     setInput("def fn():", "  stmt1", "  stmt2", "<caret>");
     pressEnterAndAssertResult("def fn():", "  stmt1", "  stmt2", "", "<caret>");
   }
 
+  @Test
   public void testIndentAfterIf() {
     setInput("if condition:<caret>");
     pressEnterAndAssertResult("if condition:", "  <caret>");
   }
 
+  @Test
   public void testNoIndentAfterIfPlusStatement() {
     setInput("if condition:stmt<caret>");
     pressEnterAndAssertResult("if condition:stmt", "<caret>");
   }
 
+  @Test
   public void testIndentAfterElseIf() {
     setInput("if condition:", "  stmt", "elif:<caret>");
     pressEnterAndAssertResult("if condition:", "  stmt", "elif:", "  <caret>");
   }
 
+  @Test
   public void testNoIndentAfterElseIfPlusStatement() {
     setInput("if condition:", "  stmt", "elif:stmt<caret>");
     pressEnterAndAssertResult("if condition:", "  stmt", "elif:stmt", "<caret>");
   }
 
+  @Test
   public void testIndentAfterElse() {
     setInput("if condition:", "  stmt", "else:<caret>");
     pressEnterAndAssertResult("if condition:", "  stmt", "else:", "  <caret>");
   }
 
+  @Test
   public void testNoIndentAfterElsePlusStatement() {
     setInput("if condition:", "  stmt", "else:stmt<caret>");
     pressEnterAndAssertResult("if condition:", "  stmt", "else:stmt", "<caret>");
   }
 
+  @Test
   public void testIndentAfterForColon() {
     setInput("for x in list:<caret>");
     pressEnterAndAssertResult("for x in list:", "  <caret>");
   }
 
+  @Test
   public void testNoIndentAfterForPlusStatement() {
     setInput("for x in list:do_action<caret>");
     pressEnterAndAssertResult("for x in list:do_action", "<caret>");
   }
 
+  @Test
   public void testCommonRuleCase1() {
     setInput("java_library(", "    name = 'lib'", "    srcs = [<caret>]");
     pressEnterAndAssertResult(
         "java_library(", "    name = 'lib'", "    srcs = [", "        <caret>", "    ]");
   }
 
+  @Test
   public void testCommonRuleCase2() {
     setInput(
         "java_library(", "    name = 'lib'", "    srcs = [", "        'source',<caret>", "    ]");
@@ -231,43 +273,51 @@
         "    ]");
   }
 
+  @Test
   public void testCommonRuleCase3() {
     setInput("java_library(", "    name = 'lib'", "    srcs = ['first',<caret>]");
     pressEnterAndAssertResult(
         "java_library(", "    name = 'lib'", "    srcs = ['first',", "            <caret>]");
   }
 
+  @Test
   public void testDedentAfterReturn() {
     setInput("def fn():", "  return None<caret>");
     pressEnterAndAssertResult("def fn():", "  return None", "<caret>");
   }
 
+  @Test
   public void testDedentAfterEmptyReturn() {
     setInput("def fn():", "  return<caret>");
     pressEnterAndAssertResult("def fn():", "  return", "<caret>");
   }
 
+  @Test
   public void testDedentAfterReturnWithTrailingWhitespace() {
     setInput("def fn():", "  return<caret>   ");
     pressEnterAndAssertResult("def fn():", "  return", "<caret>");
   }
 
+  @Test
   public void testDedentAfterComplexReturn() {
     setInput("def fn():", "  return a == b<caret>");
     pressEnterAndAssertResult("def fn():", "  return a == b", "<caret>");
   }
 
+  @Test
   public void testDedentAfterPass() {
     setInput("def fn():", "  pass<caret>");
     pressEnterAndAssertResult("def fn():", "  pass", "<caret>");
   }
 
+  @Test
   public void testDedentAfterPassInLoop() {
     setInput("def fn():", "  for a in (1,2,3):", "    pass<caret>");
     pressEnterAndAssertResult("def fn():", "  for a in (1,2,3):", "    pass", "  <caret>");
   }
 
   // regression test for b/29564041
+  @Test
   public void testNoExceptionPressingEnterAtStartOfFile() {
     setInput("#<caret>");
     pressEnterAndAssertResult("#", "<caret>");
diff --git a/base/tests/integrationtests/com/google/idea/blaze/base/lang/buildfile/editor/BuildQuoteHandlerTest.java b/base/tests/integrationtests/com/google/idea/blaze/base/lang/buildfile/editor/BuildQuoteHandlerTest.java
index 459d942..693ea1b 100644
--- a/base/tests/integrationtests/com/google/idea/blaze/base/lang/buildfile/editor/BuildQuoteHandlerTest.java
+++ b/base/tests/integrationtests/com/google/idea/blaze/base/lang/buildfile/editor/BuildQuoteHandlerTest.java
@@ -18,10 +18,15 @@
 import com.google.common.base.Joiner;
 import com.google.idea.blaze.base.lang.buildfile.BuildFileIntegrationTestCase;
 import com.google.idea.blaze.base.lang.buildfile.psi.BuildFile;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
 
 /** Tests for BuildQuoteHandler. */
+@RunWith(JUnit4.class)
 public class BuildQuoteHandlerTest extends BuildFileIntegrationTestCase {
 
+  @Test
   public void testClosingQuoteInserted() {
     BuildFile file = createBuildFile("BUILD", "");
 
@@ -29,6 +34,7 @@
     assertFileContents(file, "\"\"");
   }
 
+  @Test
   public void testClosingSingleQuoteInserted() {
     BuildFile file = createBuildFile("BUILD", "");
 
@@ -36,6 +42,7 @@
     assertFileContents(file, "''");
   }
 
+  @Test
   public void testClosingTripleQuoteInserted() {
     BuildFile file = createBuildFile("BUILD", "");
 
@@ -45,6 +52,7 @@
     assertFileContents(file, "\"\"\"\"\"\"");
   }
 
+  @Test
   public void testClosingTripleSingleQuoteInserted() {
     BuildFile file = createBuildFile("BUILD", "");
 
@@ -54,6 +62,7 @@
     assertFileContents(file, "''''''");
   }
 
+  @Test
   public void testOnlyCaretMovedWhenCompletingExistingClosingQuotes() {
     BuildFile file = createBuildFile("BUILD", "'text<caret>'", "laterContents");
 
@@ -64,6 +73,7 @@
     testFixture.checkResult(Joiner.on("\n").join("'text'<caret>", "laterContents"));
   }
 
+  @Test
   public void testOnlyCaretMovedWhenCompletingExistingClosingTripleQuotes() {
     BuildFile file = createBuildFile("BUILD", "'''text<caret>'''", "laterContents");
 
@@ -82,6 +92,7 @@
     testFixture.checkResult(Joiner.on("\n").join("'''text'''<caret>", "laterContents"));
   }
 
+  @Test
   public void testAdditionalTripleQuotesNotInsertedWhenClosingQuotes() {
     BuildFile file = createBuildFile("BUILD", "'''text''<caret>", "laterContents");
 
@@ -92,6 +103,7 @@
     testFixture.checkResult(Joiner.on("\n").join("'''text'''<caret>", "laterContents"));
   }
 
+  @Test
   public void testAdditionalQuoteNotInsertedWhenClosingQuotes() {
     BuildFile file = createBuildFile("BUILD", "'text<caret>", "laterContents");
 
diff --git a/base/tests/integrationtests/com/google/idea/blaze/base/lang/buildfile/editor/EnterInLineCommentTest.java b/base/tests/integrationtests/com/google/idea/blaze/base/lang/buildfile/editor/EnterInLineCommentTest.java
index 554b127..2f3b0f2 100644
--- a/base/tests/integrationtests/com/google/idea/blaze/base/lang/buildfile/editor/EnterInLineCommentTest.java
+++ b/base/tests/integrationtests/com/google/idea/blaze/base/lang/buildfile/editor/EnterInLineCommentTest.java
@@ -18,10 +18,15 @@
 import com.google.idea.blaze.base.lang.buildfile.BuildFileIntegrationTestCase;
 import com.google.idea.blaze.base.lang.buildfile.psi.BuildFile;
 import com.intellij.openapi.editor.Editor;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
 
 /** Test that comments are continued when creating a newline mid comment. */
+@RunWith(JUnit4.class)
 public class EnterInLineCommentTest extends BuildFileIntegrationTestCase {
 
+  @Test
   public void testInternalNewlineCommented() {
     BuildFile file = createBuildFile("BUILD", "# first line comment", "# second line comment");
 
@@ -32,6 +37,7 @@
     assertCaretPosition(editor, 2, 2);
   }
 
+  @Test
   public void testNewlineAtEndOfComment() {
     BuildFile file = createBuildFile("BUILD", "# first line comment", "# second line comment");
 
diff --git a/base/tests/integrationtests/com/google/idea/blaze/base/lang/buildfile/findusages/BlazePackageFindUsagesTest.java b/base/tests/integrationtests/com/google/idea/blaze/base/lang/buildfile/findusages/BlazePackageFindUsagesTest.java
index c7f9152..f7a52f0 100644
--- a/base/tests/integrationtests/com/google/idea/blaze/base/lang/buildfile/findusages/BlazePackageFindUsagesTest.java
+++ b/base/tests/integrationtests/com/google/idea/blaze/base/lang/buildfile/findusages/BlazePackageFindUsagesTest.java
@@ -23,13 +23,18 @@
 import com.google.idea.blaze.base.lang.buildfile.search.FindUsages;
 import com.intellij.psi.PsiElement;
 import com.intellij.psi.PsiReference;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
 
 /**
  * Tests that all references to a blaze package (including in the package components of labels) are
  * found by the 'Find Usages' action.
  */
+@RunWith(JUnit4.class)
 public class BlazePackageFindUsagesTest extends BuildFileIntegrationTestCase {
 
+  @Test
   public void testDirectReferenceFound() {
     BuildFile foo = createBuildFile("java/com/google/foo/BUILD");
 
@@ -46,6 +51,7 @@
     assertThat(ref.getContainingFile()).isEqualTo(bar);
   }
 
+  @Test
   public void testLabelFragmentReferenceFound() {
     BuildFile foo = createBuildFile("java/com/google/foo/BUILD", "java_library(name = \"lib\")");
 
@@ -63,6 +69,7 @@
   }
 
   /** If these don't resolve, directory rename refactoring won't update all labels correctly */
+  @Test
   public void testInternalReferencesResolve() {
     BuildFile buildFile =
         createBuildFile(
diff --git a/base/tests/integrationtests/com/google/idea/blaze/base/lang/buildfile/findusages/ExternalFileUsagesTest.java b/base/tests/integrationtests/com/google/idea/blaze/base/lang/buildfile/findusages/ExternalFileUsagesTest.java
index a02d996..cdb0c64 100644
--- a/base/tests/integrationtests/com/google/idea/blaze/base/lang/buildfile/findusages/ExternalFileUsagesTest.java
+++ b/base/tests/integrationtests/com/google/idea/blaze/base/lang/buildfile/findusages/ExternalFileUsagesTest.java
@@ -27,13 +27,18 @@
 import com.intellij.psi.PsiElement;
 import com.intellij.psi.PsiFile;
 import com.intellij.psi.PsiReference;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
 
 /**
  * Tests that references to external files (e.g. Java classes, text files) are found by the 'Find
  * Usages' action
  */
+@RunWith(JUnit4.class)
 public class ExternalFileUsagesTest extends BuildFileIntegrationTestCase {
 
+  @Test
   public void testJavaClassUsagesFound() {
     PsiFile javaFile =
         createPsiFile(
@@ -56,31 +61,31 @@
     assertThat(PsiUtils.getParentOfType(ref, Argument.Keyword.class)).isEqualTo(arg);
   }
 
+  @Test
   public void testTextFileUsagesFound() {
     PsiFile textFile = createPsiFile("com/google/foo/data.txt");
 
-    BuildFile buildFile =
-        createBuildFile(
-            "com/google/foo/BUILD",
-            "filegroup(name = \"lib\", srcs = [\"data.txt\"])",
-            "filegroup(name = \"lib2\", srcs = [\"//com/google/foo:data.txt\"])");
+    createBuildFile(
+        "com/google/foo/BUILD",
+        "filegroup(name = \"lib\", srcs = [\"data.txt\"])",
+        "filegroup(name = \"lib2\", srcs = [\"//com/google/foo:data.txt\"])");
 
     PsiReference[] references = FindUsages.findAllReferences(textFile);
     assertThat(references).hasLength(2);
   }
 
+  @Test
   public void testInvalidReferenceDoesntResolve() {
-    BuildFile packageFoo = createBuildFile("com/google/foo/BUILD");
+    createBuildFile("com/google/foo/BUILD");
     PsiFile textFileInFoo = createPsiFile("com/google/foo/data.txt");
 
-    BuildFile packageBar =
-        createBuildFile(
-            "com/google/bar/BUILD", "filegroup(name = \"lib\", srcs = [\":data.txt\"])");
+    createBuildFile("com/google/bar/BUILD", "filegroup(name = \"lib\", srcs = [\":data.txt\"])");
 
     PsiReference[] references = FindUsages.findAllReferences(textFileInFoo);
     assertThat(references).isEmpty();
   }
 
+  @Test
   public void testSkylarkExtensionUsagesFound() {
     BuildFile ext = createBuildFile("com/google/foo/ext.bzl", "def fn(): return");
     createBuildFile(
@@ -93,6 +98,7 @@
     assertThat(references).hasLength(3);
   }
 
+  @Test
   public void testSkylarkExtensionInSubDirectoryUsagesFound() {
     BuildFile ext = createBuildFile("com/google/foo/subdir/ext.bzl", "def fn(): return");
     createBuildFile(
@@ -105,8 +111,9 @@
     assertThat(references).hasLength(3);
   }
 
+  @Test
   public void testSkylarkExtensionInSubDirectoryOfDifferentPackage() {
-    BuildFile otherPkg = createBuildFile("com/google/foo/BUILD");
+    createBuildFile("com/google/foo/BUILD");
     BuildFile ext = createBuildFile("com/google/foo/subdir/ext.bzl", "def fn(): return");
 
     createBuildFile("com/google/bar/BUILD", "load('//com/google/foo:subdir/ext.bzl', 'fn')");
@@ -115,11 +122,11 @@
     assertThat(references).hasLength(1);
   }
 
+  @Test
   public void testSkylarkExtensionReferencedFromSubpackage() {
-    BuildFile pkg = createBuildFile("com/google/foo/BUILD");
+    createBuildFile("com/google/foo/BUILD");
     BuildFile ext1 = createBuildFile("com/google/foo/subdir/testing.bzl", "def fn(): return");
-    BuildFile ext2 =
-        createBuildFile("com/google/foo/subdir/other.bzl", "load(':subdir/testing.bzl', 'fn')");
+    createBuildFile("com/google/foo/subdir/other.bzl", "load(':subdir/testing.bzl', 'fn')");
 
     PsiReference[] references = FindUsages.findAllReferences(ext1);
     assertThat(references).hasLength(1);
diff --git a/base/tests/integrationtests/com/google/idea/blaze/base/lang/buildfile/findusages/FindParameterUsagesTest.java b/base/tests/integrationtests/com/google/idea/blaze/base/lang/buildfile/findusages/FindParameterUsagesTest.java
index b16e666..8928bef 100644
--- a/base/tests/integrationtests/com/google/idea/blaze/base/lang/buildfile/findusages/FindParameterUsagesTest.java
+++ b/base/tests/integrationtests/com/google/idea/blaze/base/lang/buildfile/findusages/FindParameterUsagesTest.java
@@ -23,12 +23,17 @@
 import com.google.idea.blaze.base.lang.buildfile.psi.ParameterList;
 import com.google.idea.blaze.base.lang.buildfile.search.FindUsages;
 import com.intellij.psi.PsiReference;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
 
 /**
  * Tests that usages of function parameters (i.e. by named args in funcall expressions) are found
  */
+@RunWith(JUnit4.class)
 public class FindParameterUsagesTest extends BuildFileIntegrationTestCase {
 
+  @Test
   public void testLocalReferences() {
     BuildFile buildFile =
         createBuildFile(
@@ -46,6 +51,7 @@
     assertThat(references).hasLength(1);
   }
 
+  @Test
   public void testNonLocalReferences() {
     BuildFile foo = createBuildFile("java/com/google/build_defs.bzl", "def function(arg1, arg2)");
 
diff --git a/base/tests/integrationtests/com/google/idea/blaze/base/lang/buildfile/findusages/FindRuleUsagesTest.java b/base/tests/integrationtests/com/google/idea/blaze/base/lang/buildfile/findusages/FindRuleUsagesTest.java
index 4d69567..1e81970 100644
--- a/base/tests/integrationtests/com/google/idea/blaze/base/lang/buildfile/findusages/FindRuleUsagesTest.java
+++ b/base/tests/integrationtests/com/google/idea/blaze/base/lang/buildfile/findusages/FindRuleUsagesTest.java
@@ -27,10 +27,15 @@
 import com.intellij.codeInsight.navigation.actions.GotoDeclarationAction;
 import com.intellij.psi.PsiElement;
 import com.intellij.psi.PsiReference;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
 
 /** Tests that usages of build rules are found */
+@RunWith(JUnit4.class)
 public class FindRuleUsagesTest extends BuildFileIntegrationTestCase {
 
+  @Test
   public void testLocalReferences() {
     BuildFile buildFile =
         createBuildFile(
@@ -54,6 +59,7 @@
   }
 
   // test full package references, made locally
+  @Test
   public void testLocalFullReference() {
     BuildFile buildFile =
         createBuildFile(
@@ -71,6 +77,7 @@
     assertThat(ref.getParent()).isInstanceOf(ListLiteral.class);
   }
 
+  @Test
   public void testNonLocalReferences() {
     BuildFile targetFile =
         createBuildFile("java/com/google/foo/BUILD", "java_library(name = \"target\")");
@@ -90,6 +97,7 @@
     assertThat(ref.getContainingFile()).isEqualTo(refFile);
   }
 
+  @Test
   public void testFindUsagesWorksFromNameString() {
     BuildFile targetFile =
         createBuildFile("java/com/google/foo/BUILD", "java_library(name = \"tar<caret>get\")");
@@ -113,14 +121,14 @@
     assertThat(ref.getContainingFile()).isEqualTo(refFile);
   }
 
+  @Test
   public void testInvalidReferenceDoesntResolve() {
     // reference ":target" from another build file (missing package path in label)
     BuildFile targetFile =
         createBuildFile("java/com/google/foo/BUILD", "java_library(name = \"target\")");
 
-    BuildFile refFile =
-        createBuildFile(
-            "java/com/google/bar/BUILD", "java_library(name = \"ref\", exports = [\":target\"])");
+    createBuildFile(
+        "java/com/google/bar/BUILD", "java_library(name = \"ref\", exports = [\":target\"])");
 
     FuncallExpression target = targetFile.findChildByClass(FuncallExpression.class);
     assertThat(target).isNotNull();
diff --git a/base/tests/integrationtests/com/google/idea/blaze/base/lang/buildfile/findusages/FunctionStatementUsagesTest.java b/base/tests/integrationtests/com/google/idea/blaze/base/lang/buildfile/findusages/FunctionStatementUsagesTest.java
index 22be9b5..b03f32d 100644
--- a/base/tests/integrationtests/com/google/idea/blaze/base/lang/buildfile/findusages/FunctionStatementUsagesTest.java
+++ b/base/tests/integrationtests/com/google/idea/blaze/base/lang/buildfile/findusages/FunctionStatementUsagesTest.java
@@ -26,10 +26,15 @@
 import com.google.idea.blaze.base.lang.buildfile.search.FindUsages;
 import com.intellij.psi.PsiElement;
 import com.intellij.psi.PsiReference;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
 
 /** Tests that usages of function declarations are found */
+@RunWith(JUnit4.class)
 public class FunctionStatementUsagesTest extends BuildFileIntegrationTestCase {
 
+  @Test
   public void testLocalReferences() {
     BuildFile buildFile =
         createBuildFile(
@@ -47,6 +52,7 @@
     assertThat(ref).isInstanceOf(FuncallExpression.class);
   }
 
+  @Test
   public void testLoadedFunctionReferences() {
     BuildFile extFile =
         createBuildFile("java/com/google/build_defs.bzl", "def function(name, deps)");
@@ -70,6 +76,7 @@
     assertThat(ref.getParent()).isEqualTo(load);
   }
 
+  @Test
   public void testFuncallReference() {
     BuildFile extFile =
         createBuildFile("java/com/google/tools/build_defs.bzl", "def function(name, deps)");
diff --git a/base/tests/integrationtests/com/google/idea/blaze/base/lang/buildfile/findusages/GlobFindUsagesTest.java b/base/tests/integrationtests/com/google/idea/blaze/base/lang/buildfile/findusages/GlobFindUsagesTest.java
index c1de37e..f42ebe5 100644
--- a/base/tests/integrationtests/com/google/idea/blaze/base/lang/buildfile/findusages/GlobFindUsagesTest.java
+++ b/base/tests/integrationtests/com/google/idea/blaze/base/lang/buildfile/findusages/GlobFindUsagesTest.java
@@ -30,19 +30,25 @@
 import com.intellij.psi.impl.PsiManagerEx;
 import com.intellij.psi.impl.file.impl.FileManager;
 import com.intellij.testFramework.LightVirtualFile;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
 
 /** Tests that file references in globs are included in the 'find usages' results. */
+@RunWith(JUnit4.class)
 public class GlobFindUsagesTest extends BuildFileIntegrationTestCase {
 
+  @Test
   public void testSimpleGlobReferencingSingleFile() {
     PsiFile ref = createPsiFile("java/com/google/Test.java");
-    BuildFile file = createBuildFile("java/com/google/BUILD", "glob(['**/*.java'])");
+    createBuildFile("java/com/google/BUILD", "glob(['**/*.java'])");
 
     PsiReference[] references = FindUsages.findAllReferences(ref);
     assertThat(references).hasLength(1);
     assertThat(references[0].getElement()).isInstanceOf(GlobExpression.class);
   }
 
+  @Test
   public void testSimpleGlobReferencingSingleFile2() {
     PsiFile ref = createPsiFile("java/com/google/Test.java");
     BuildFile file = createBuildFile("java/com/google/BUILD", "glob(['*.java'])");
@@ -54,6 +60,7 @@
     assertThat(references[0].getElement()).isEqualTo(glob);
   }
 
+  @Test
   public void testSimpleGlobReferencingSingleFile3() {
     PsiFile ref = createPsiFile("java/com/google/Test.java");
     BuildFile file = createBuildFile("java/com/google/BUILD", "glob(['T*t.java'])");
@@ -65,6 +72,7 @@
     assertThat(references[0].getElement()).isEqualTo(glob);
   }
 
+  @Test
   public void testGlobReferencingMultipleFiles() {
     PsiFile ref1 = createPsiFile("java/com/google/Test.java");
     PsiFile ref2 = createPsiFile("java/com/google/Foo.java");
@@ -81,6 +89,7 @@
     assertThat(references[0].getElement()).isEqualTo(glob);
   }
 
+  @Test
   public void testFindsSubDirectories() {
     PsiFile ref1 = createPsiFile("java/com/google/test/Test.java");
     BuildFile file = createBuildFile("java/com/google/BUILD", "glob(['**/*.java'])");
@@ -92,6 +101,7 @@
     assertThat(references[0].getElement()).isEqualTo(glob);
   }
 
+  @Test
   public void testGlobWithExcludes() {
     PsiFile test = createPsiFile("java/com/google/tests/Test.java");
     PsiFile foo = createPsiFile("java/com/google/Foo.java");
@@ -109,10 +119,11 @@
     assertThat(FindUsages.findAllReferences(test)).isEmpty();
   }
 
+  @Test
   public void testIncludeDirectories() {
     PsiDirectory dir = createPsiDirectory("java/com/google/tests");
-    PsiFile test = createPsiFile("java/com/google/tests/Test.java");
-    PsiFile foo = createPsiFile("java/com/google/Foo.java");
+    createPsiFile("java/com/google/tests/Test.java");
+    createPsiFile("java/com/google/Foo.java");
     BuildFile file =
         createBuildFile(
             "java/com/google/BUILD",
@@ -125,34 +136,37 @@
     assertThat(references[0].getElement()).isEqualTo(glob);
   }
 
+  @Test
   public void testExcludeDirectories() {
     PsiDirectory dir = createPsiDirectory("java/com/google/tests");
-    PsiFile test = createPsiFile("java/com/google/tests/Test.java");
-    PsiFile foo = createPsiFile("java/com/google/Foo.java");
+    createPsiFile("java/com/google/tests/Test.java");
+    createPsiFile("java/com/google/Foo.java");
     BuildFile file =
         createBuildFile(
             "java/com/google/BUILD", "glob(" + "  ['**/*']," + "  exclude = ['BUILD'])");
 
-    GlobExpression glob = PsiUtils.findFirstChildOfClassRecursive(file, GlobExpression.class);
+    PsiUtils.findFirstChildOfClassRecursive(file, GlobExpression.class);
 
     PsiReference[] references = FindUsages.findAllReferences(dir);
     assertThat(references).isEmpty();
   }
 
+  @Test
   public void testFilesInSubpackagesExcluded() {
     BuildFile pkg = createBuildFile("java/com/google/BUILD", "glob(['**/*.java'])");
     BuildFile subPkg = createBuildFile("java/com/google/other/BUILD");
     createFile("java/com/google/other/Other.java");
 
-    GlobExpression glob = PsiUtils.findFirstChildOfClassRecursive(pkg, GlobExpression.class);
+    PsiUtils.findFirstChildOfClassRecursive(pkg, GlobExpression.class);
 
     PsiReference[] references = FindUsages.findAllReferences(subPkg);
     assertThat(references).isEmpty();
   }
 
   // regression test for b/29267289
+  @Test
   public void testInMemoryFileHandledGracefully() {
-    BuildFile pkg = createBuildFile("java/com/google/BUILD", "glob(['**/*.java'])");
+    createBuildFile("java/com/google/BUILD", "glob(['**/*.java'])");
 
     LightVirtualFile inMemoryFile =
         new LightVirtualFile("mockProjectViewFile", ProjectViewLanguage.INSTANCE, "");
@@ -164,6 +178,6 @@
 
     PsiFile psiFile = fileManager.findFile(inMemoryFile);
 
-    PsiReference[] references = FindUsages.findAllReferences(psiFile);
+    FindUsages.findAllReferences(psiFile);
   }
 }
diff --git a/base/tests/integrationtests/com/google/idea/blaze/base/lang/buildfile/findusages/LocalVariableUsagesTest.java b/base/tests/integrationtests/com/google/idea/blaze/base/lang/buildfile/findusages/LocalVariableUsagesTest.java
index 50fafb4..9122f05 100644
--- a/base/tests/integrationtests/com/google/idea/blaze/base/lang/buildfile/findusages/LocalVariableUsagesTest.java
+++ b/base/tests/integrationtests/com/google/idea/blaze/base/lang/buildfile/findusages/LocalVariableUsagesTest.java
@@ -29,13 +29,18 @@
 import com.google.idea.blaze.base.lang.buildfile.search.FindUsages;
 import com.intellij.psi.PsiElement;
 import com.intellij.psi.PsiReference;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
 
 /**
  * Tests that references to local variables are found by the 'Find Usages' action TODO: Support
  * comprehension suffix, and add test for it
  */
+@RunWith(JUnit4.class)
 public class LocalVariableUsagesTest extends BuildFileIntegrationTestCase {
 
+  @Test
   public void testLocalReferences() {
     BuildFile buildFile =
         createBuildFile(
@@ -66,6 +71,7 @@
   }
 
   // the case where a symbol is the target of multiple assignment statements
+  @Test
   public void testMultipleAssignments() {
     BuildFile buildFile =
         createBuildFile("java/com/google/BUILD", "var = 5", "var += 1", "var = 0");
diff --git a/base/tests/integrationtests/com/google/idea/blaze/base/lang/buildfile/formatting/BuildFileFoldingBuilderTest.java b/base/tests/integrationtests/com/google/idea/blaze/base/lang/buildfile/formatting/BuildFileFoldingBuilderTest.java
index 8581cbc..f9ac821 100644
--- a/base/tests/integrationtests/com/google/idea/blaze/base/lang/buildfile/formatting/BuildFileFoldingBuilderTest.java
+++ b/base/tests/integrationtests/com/google/idea/blaze/base/lang/buildfile/formatting/BuildFileFoldingBuilderTest.java
@@ -22,10 +22,15 @@
 import com.google.idea.blaze.base.lang.buildfile.psi.LoadStatement;
 import com.intellij.lang.folding.FoldingDescriptor;
 import com.intellij.openapi.editor.Editor;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
 
 /** Tests for {@link BuildFileFoldingBuilder}. */
+@RunWith(JUnit4.class)
 public class BuildFileFoldingBuilderTest extends BuildFileIntegrationTestCase {
 
+  @Test
   public void testEndOfFileFunctionDelcaration() {
     // bug 28618935: test no NPE in the case where there's no
     // statement list following the func-def colon
@@ -34,6 +39,7 @@
     getFoldingRegions(file);
   }
 
+  @Test
   public void testFuncDefStatementsFolded() {
     BuildFile file =
         createBuildFile(
@@ -52,6 +58,7 @@
         .isEqualTo(file.findFunctionInScope("function"));
   }
 
+  @Test
   public void testRulesFolded() {
     BuildFile file =
         createBuildFile(
@@ -66,6 +73,7 @@
     assertThat(foldingRegions[0].getElement().getPsi()).isEqualTo(file.findRule("lib"));
   }
 
+  @Test
   public void testLoadStatementFolded() {
     BuildFile file =
         createBuildFile(
diff --git a/base/tests/integrationtests/com/google/idea/blaze/base/lang/buildfile/language/BuildFileTypeTest.java b/base/tests/integrationtests/com/google/idea/blaze/base/lang/buildfile/language/BuildFileTypeTest.java
index 85b4d65..50a490a 100644
--- a/base/tests/integrationtests/com/google/idea/blaze/base/lang/buildfile/language/BuildFileTypeTest.java
+++ b/base/tests/integrationtests/com/google/idea/blaze/base/lang/buildfile/language/BuildFileTypeTest.java
@@ -20,15 +20,21 @@
 import com.google.idea.blaze.base.lang.buildfile.BuildFileIntegrationTestCase;
 import com.google.idea.blaze.base.lang.buildfile.psi.BuildFile;
 import com.intellij.psi.PsiFile;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
 
 /** Tests that BUILD files are recognized as such */
+@RunWith(JUnit4.class)
 public class BuildFileTypeTest extends BuildFileIntegrationTestCase {
 
+  @Test
   public void testSkylarkExtensionRecognized() {
     PsiFile file = createPsiFile("java/com/google/foo/build_defs.bzl");
     assertThat(file).isInstanceOf(BuildFile.class);
   }
 
+  @Test
   public void testExactNameMatch() {
     PsiFile file = createPsiFile("java/com/google/foo/BUILD");
     assertThat(file).isInstanceOf(BuildFile.class);
@@ -40,6 +46,7 @@
    * Currently, turned off by default because references won't resolve correctly -- they'll point
    * back to normal BUILD files.
    */
+  @Test
   public void testOtherBuildFilesNotRecognized() {
     PsiFile file = createPsiFile("java/com/google/foo/BUILD.tools");
     assertThat(file).isNotInstanceOf(BuildFile.class);
diff --git a/base/tests/integrationtests/com/google/idea/blaze/base/lang/buildfile/parser/BuildParserTest.java b/base/tests/integrationtests/com/google/idea/blaze/base/lang/buildfile/parser/BuildParserTest.java
index 10b7869..1e09bc6 100644
--- a/base/tests/integrationtests/com/google/idea/blaze/base/lang/buildfile/parser/BuildParserTest.java
+++ b/base/tests/integrationtests/com/google/idea/blaze/base/lang/buildfile/parser/BuildParserTest.java
@@ -35,17 +35,23 @@
 import java.util.Arrays;
 import java.util.List;
 import java.util.stream.Collectors;
+import org.junit.After;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
 
 /** Test for the BUILD file parser (converting lexical elements into PSI elements) */
+@RunWith(JUnit4.class)
 public class BuildParserTest extends BuildFileIntegrationTestCase {
 
   private final List<String> errors = Lists.newArrayList();
 
-  @Override
-  protected void doTearDown() {
+  @After
+  public final void doTearDown() {
     errors.clear();
   }
 
+  @Test
   public void testAugmentedAssign() throws Exception {
     assertThat(parse("x += 1")).isEqualTo("aug_assign(reference, int)");
     assertThat(parse("x -= 1")).isEqualTo("aug_assign(reference, int)");
@@ -55,11 +61,13 @@
     assertNoErrors();
   }
 
+  @Test
   public void testAssign() throws Exception {
     assertThat(parse("a, b = 5\n")).isEqualTo("assignment(list(reference, target), int)");
     assertNoErrors();
   }
 
+  @Test
   public void testAssign2() throws Exception {
     assertThat(parse("a = b;c = d\n"))
         .isEqualTo(
@@ -67,11 +75,13 @@
     assertNoErrors();
   }
 
+  @Test
   public void testInvalidAssign() throws Exception {
     parse("1 + (b = c)");
     assertContainsErrors();
   }
 
+  @Test
   public void testTupleAssign() throws Exception {
     assertThat(parse("list[0] = 5; dict['key'] = value\n"))
         .isEqualTo(
@@ -82,18 +92,21 @@
     assertNoErrors();
   }
 
+  @Test
   public void testPrimary() throws Exception {
     assertThat(parse("f(1 + 2)"))
         .isEqualTo("function_call(reference, arg_list(positional(binary_op(int, int))))");
     assertNoErrors();
   }
 
+  @Test
   public void testSecondary() throws Exception {
     assertThat(parse("f(1 % 2)"))
         .isEqualTo("function_call(reference, arg_list(positional(binary_op(int, int))))");
     assertNoErrors();
   }
 
+  @Test
   public void testDoesNotGetStuck() throws Exception {
     // Make sure the parser does not get stuck when trying
     // to parse an expression containing a syntax error.
@@ -103,6 +116,7 @@
     parse("f(1, [x for foo foo foo foo], 3)");
   }
 
+  @Test
   public void testInvalidFunctionStatementDoesNotGetStuck() throws Exception {
     // Make sure the parser does not get stuck when trying
     // to parse a function statement containing a syntax error.
@@ -111,6 +125,7 @@
     parse("def empty)");
   }
 
+  @Test
   public void testSubstring() throws Exception {
     assertThat(parse("'FOO.CC'[:].lower()[1:]"))
         .isEqualTo(
@@ -122,6 +137,7 @@
     assertNoErrors();
   }
 
+  @Test
   public void testFuncallExpr() throws Exception {
     assertThat(parse("foo(1, 2, bar=wiz)"))
         .isEqualTo(
@@ -134,6 +150,7 @@
     assertNoErrors();
   }
 
+  @Test
   public void testMethCallExpr() throws Exception {
     assertThat(parse("foo.foo(1, 2, bar=wiz)"))
         .isEqualTo(
@@ -144,6 +161,7 @@
     assertNoErrors();
   }
 
+  @Test
   public void testChainedMethCallExpr() throws Exception {
     assertThat(parse("foo.replace().split(1)"))
         .isEqualTo(
@@ -152,16 +170,19 @@
     assertNoErrors();
   }
 
+  @Test
   public void testPropRefExpr() throws Exception {
     assertThat(parse("foo.foo")).isEqualTo("dot_expr(reference, reference)");
     assertNoErrors();
   }
 
+  @Test
   public void testStringMethExpr() throws Exception {
     assertThat(parse("'foo'.foo()")).isEqualTo("function_call(string, reference, arg_list)");
     assertNoErrors();
   }
 
+  @Test
   public void testFuncallLocation() throws Exception {
     assertThat(parse("a(b);c = d\n"))
         .isEqualTo(
@@ -172,12 +193,14 @@
     assertNoErrors();
   }
 
+  @Test
   public void testList() throws Exception {
     assertThat(parse("[0,f(1),2]"))
         .isEqualTo("list(int, function_call(reference, arg_list(positional(int))), int)");
     assertNoErrors();
   }
 
+  @Test
   public void testDict() throws Exception {
     assertThat(parse("{1:2,2:f(1),3:4}"))
         .isEqualTo(
@@ -191,6 +214,7 @@
     assertNoErrors();
   }
 
+  @Test
   public void testArgumentList() throws Exception {
     assertThat(parse("f(0,g(1,2),2)"))
         .isEqualTo(
@@ -204,6 +228,7 @@
     assertNoErrors();
   }
 
+  @Test
   public void testForBreakContinue() throws Exception {
     String parsed =
         parse("def foo():", "  for i in [1, 2]:", "    break", "    continue", "    break");
@@ -217,37 +242,44 @@
     assertNoErrors();
   }
 
+  @Test
   public void testEmptyTuple() throws Exception {
     assertThat(parse("()")).isEqualTo("list");
     assertNoErrors();
   }
 
+  @Test
   public void testTupleTrailingComma() throws Exception {
     assertThat(parse("(42,)")).isEqualTo("list(int)");
     assertNoErrors();
   }
 
+  @Test
   public void testSingleton() throws Exception {
     assertThat(parse("(42)")) // not a tuple!
         .isEqualTo("list(int)");
     assertNoErrors();
   }
 
+  @Test
   public void testDictionaryLiterals() throws Exception {
     assertThat(parse("{1:42}")).isEqualTo("dict(dict_entry(int, int))");
     assertNoErrors();
   }
 
+  @Test
   public void testDictionaryLiterals1() throws Exception {
     assertThat(parse("{}")).isEqualTo("dict");
     assertNoErrors();
   }
 
+  @Test
   public void testDictionaryLiterals2() throws Exception {
     assertThat(parse("{1:42,}")).isEqualTo("dict(dict_entry(int, int))");
     assertNoErrors();
   }
 
+  @Test
   public void testDictionaryLiterals3() throws Exception {
     assertThat(parse("{1:42,2:43,3:44}"))
         .isEqualTo(
@@ -260,11 +292,13 @@
     assertNoErrors();
   }
 
+  @Test
   public void testInvalidListComprehensionSyntax() throws Exception {
     assertThat(parse("[x for x for y in ['a']]")).isEqualTo("list_comp(reference, reference)");
     assertContainsErrors();
   }
 
+  @Test
   public void testListComprehensionEmptyList() throws Exception {
     // At the moment, we just parse the components of comprehension suffixes.
     assertThat(parse("['foo/%s.java' % x for x in []]"))
@@ -272,6 +306,7 @@
     assertNoErrors();
   }
 
+  @Test
   public void testListComprehension() throws Exception {
     assertThat(parse("['foo/%s.java' % x for x in ['bar', 'wiz', 'quux']]"))
         .isEqualTo(
@@ -283,6 +318,7 @@
     assertNoErrors();
   }
 
+  @Test
   public void testDoesntGetStuck2() throws Exception {
     parse(
         "def foo():",
@@ -298,6 +334,7 @@
     assertContainsErrors();
   }
 
+  @Test
   public void testDoesntGetStuck3() throws Exception {
     parse("load(*)");
     parse("load()");
@@ -308,6 +345,7 @@
     assertContainsErrors();
   }
 
+  @Test
   public void testExprAsStatement() throws Exception {
     String parsed =
         parse("li = []", "li.append('a.c')", "\"\"\" string comment \"\"\"", "foo(bar)");
@@ -322,46 +360,54 @@
     assertNoErrors();
   }
 
+  @Test
   public void testPrecedence1() {
     assertThat(parse("'%sx' % 'foo' + 'bar'"))
         .isEqualTo("binary_op(binary_op(string, string), string)");
     assertNoErrors();
   }
 
+  @Test
   public void testPrecedence2() {
     assertThat(parse("('%sx' + 'foo') * 'bar'"))
         .isEqualTo("binary_op(list(binary_op(string, string)), string)");
     assertNoErrors();
   }
 
+  @Test
   public void testPrecedence3() {
     assertThat(parse("'%sx' % ('foo' + 'bar')"))
         .isEqualTo("binary_op(string, list(binary_op(string, string)))");
     assertNoErrors();
   }
 
+  @Test
   public void testPrecedence4() throws Exception {
     assertThat(parse("1 + - (2 - 3)"))
         .isEqualTo("binary_op(int, positional(list(binary_op(int, int))))");
     assertNoErrors();
   }
 
+  @Test
   public void testPrecedence5() throws Exception {
     assertThat(parse("2 * x | y + 1"))
         .isEqualTo("binary_op(binary_op(int, reference), binary_op(reference, int))");
     assertNoErrors();
   }
 
+  @Test
   public void testNotIsIgnored() throws Exception {
     assertThat(parse("not 'b'")).isEqualTo("string");
     assertNoErrors();
   }
 
+  @Test
   public void testNotIn() throws Exception {
     assertThat(parse("'a' not in 'b'")).isEqualTo("binary_op(string, string)");
     assertNoErrors();
   }
 
+  @Test
   public void testParseBuildFileWithSingeRule() throws Exception {
     ASTNode tree =
         createAST(
@@ -375,6 +421,7 @@
     assertNoErrors();
   }
 
+  @Test
   public void testParseBuildFileWithMultipleRules() throws Exception {
     ASTNode tree =
         createAST(
@@ -393,32 +440,38 @@
     assertNoErrors();
   }
 
+  @Test
   public void testMissingComma() throws Exception {
     // missing comma after name='foo'
     parse("genrule(name = 'foo'", "   srcs = ['in'])");
     assertContainsError("',' expected");
   }
 
+  @Test
   public void testDoubleSemicolon() throws Exception {
     parse("x = 1; ; x = 2;");
     assertContainsError("expected an expression");
   }
 
+  @Test
   public void testMissingBlock() throws Exception {
     parse("x = 1;", "def foo(x):", "x = 2;\n");
     assertContainsError("'indent' expected");
   }
 
+  @Test
   public void testFunCallBadSyntax() throws Exception {
     parse("f(1,\n");
     assertContainsError("')' expected");
   }
 
+  @Test
   public void testFunCallBadSyntax2() throws Exception {
     parse("f(1, 5, ,)\n");
     assertContainsError("expected an expression");
   }
 
+  @Test
   public void testLoad() throws Exception {
     ASTNode tree = createAST("load('file', 'foo', 'bar',)\n");
     List<LoadStatement> stmts = getTopLevelNodesOfType(tree, LoadStatement.class);
@@ -430,11 +483,13 @@
     assertNoErrors();
   }
 
+  @Test
   public void testLoadNoSymbol() throws Exception {
     parse("load('/foo/bar/file')\n");
     assertContainsError("'load' statements must include at least one loaded function");
   }
 
+  @Test
   public void testFunctionDefinition() throws Exception {
     ASTNode tree =
         createAST(
@@ -449,6 +504,7 @@
     assertNoErrors();
   }
 
+  @Test
   public void testFunctionCall() throws Exception {
     ASTNode tree = createAST("function(name = 'foo', srcs, *args, **kwargs)");
     List<BuildElement> stmts = getTopLevelNodesOfType(tree, BuildElement.class);
@@ -465,6 +521,7 @@
     assertNoErrors();
   }
 
+  @Test
   public void testConditionalStatement() throws Exception {
     // we don't yet bother specifying which kind of conditionals we hit
     assertThat(parse("if x : y elif a : b else c"))
diff --git a/base/tests/integrationtests/com/google/idea/blaze/base/lang/buildfile/refactor/FileCopyTest.java b/base/tests/integrationtests/com/google/idea/blaze/base/lang/buildfile/refactor/FileCopyTest.java
index a882998..be78197 100644
--- a/base/tests/integrationtests/com/google/idea/blaze/base/lang/buildfile/refactor/FileCopyTest.java
+++ b/base/tests/integrationtests/com/google/idea/blaze/base/lang/buildfile/refactor/FileCopyTest.java
@@ -21,10 +21,15 @@
 import com.intellij.psi.PsiElement;
 import com.intellij.psi.PsiFile;
 import com.intellij.refactoring.copy.CopyHandler;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
 
 /** Tests copying files */
+@RunWith(JUnit4.class)
 public class FileCopyTest extends BuildFileIntegrationTestCase {
 
+  @Test
   public void testCopyingJavaFileReferencedByGlob() {
     createDirectory("java");
     PsiFile javaFile = createPsiFile("java/Test.java", "package java;", "public class Test {}");
diff --git a/base/tests/integrationtests/com/google/idea/blaze/base/lang/buildfile/refactor/RenameRefactoringTest.java b/base/tests/integrationtests/com/google/idea/blaze/base/lang/buildfile/refactor/RenameRefactoringTest.java
index ca3a792..5c057f7 100644
--- a/base/tests/integrationtests/com/google/idea/blaze/base/lang/buildfile/refactor/RenameRefactoringTest.java
+++ b/base/tests/integrationtests/com/google/idea/blaze/base/lang/buildfile/refactor/RenameRefactoringTest.java
@@ -31,10 +31,15 @@
 import java.util.List;
 import java.util.Set;
 import java.util.stream.Collectors;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
 
 /** Tests that BUILD file references are correctly updated when performing rename refactors. */
+@RunWith(JUnit4.class)
 public class RenameRefactoringTest extends BuildFileIntegrationTestCase {
 
+  @Test
   public void testRenameJavaClass() {
     PsiFile javaFile =
         createPsiFile(
@@ -70,6 +75,7 @@
     assertThat(expectedNewStrings).containsExactlyElementsIn(newStrings);
   }
 
+  @Test
   public void testRenameRule() {
     BuildFile fooPackage =
         createBuildFile(
@@ -98,6 +104,7 @@
         "top_level_ref = \"//com/google/foo:newTargetName\"");
   }
 
+  @Test
   public void testRenameSkylarkExtension() {
     BuildFile extFile =
         createBuildFile("java/com/google/tools/build_defs.bzl", "def function(name, deps)");
@@ -122,6 +129,7 @@
         "function(name = \"name\", deps = []");
   }
 
+  @Test
   public void testRenameLoadedFunction() {
     BuildFile extFile =
         createBuildFile("java/com/google/tools/build_defs.bzl", "def function(name, deps)");
@@ -149,6 +157,7 @@
         "action(name = \"name\", deps = []");
   }
 
+  @Test
   public void testRenameLocalVariable() {
     BuildFile file = createBuildFile("java/com/google/BUILD", "a = 1", "c = a");
 
@@ -161,6 +170,7 @@
   }
 
   // all references, including path fragments in labels, should be renamed.
+  @Test
   public void testRenameDirectory() {
     createBuildFile("java/com/baz/BUILD");
     createBuildFile("java/com/google/tools/BUILD");
@@ -184,6 +194,7 @@
         "function(name = \"name\", deps = [\"//java/alt/baz:target\"]");
   }
 
+  @Test
   public void testRenameFunctionParameter() {
     BuildFile extFile =
         createBuildFile("java/com/google/tools/build_defs.bzl", "def function(name, deps)");
@@ -212,6 +223,7 @@
         "function(name = \"name\", exports = []");
   }
 
+  @Test
   public void testRenameSuggestionForBuildFile() {
     BuildFile buildFile = createBuildFile("java/com/google/BUILD");
     RenamePsiElementProcessor processor = RenamePsiElementProcessor.forElement(buildFile);
@@ -220,6 +232,7 @@
     assertThat(suggestions[0]).isEqualTo("BUILD");
   }
 
+  @Test
   public void testRenameSuggestionForSkylarkFile() {
     BuildFile buildFile = createBuildFile("java/com/google/tools/build_defs.bzl");
     RenamePsiElementProcessor processor = RenamePsiElementProcessor.forElement(buildFile);
diff --git a/base/tests/integrationtests/com/google/idea/blaze/base/lang/buildfile/references/GlobReferenceTest.java b/base/tests/integrationtests/com/google/idea/blaze/base/lang/buildfile/references/GlobReferenceTest.java
index 0fcd82c..62a2de8 100644
--- a/base/tests/integrationtests/com/google/idea/blaze/base/lang/buildfile/references/GlobReferenceTest.java
+++ b/base/tests/integrationtests/com/google/idea/blaze/base/lang/buildfile/references/GlobReferenceTest.java
@@ -28,10 +28,15 @@
 import java.util.List;
 import java.util.Objects;
 import java.util.stream.Collectors;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
 
 /** Tests that glob references are resolved correctly. */
+@RunWith(JUnit4.class)
 public class GlobReferenceTest extends BuildFileIntegrationTestCase {
 
+  @Test
   public void testSimpleGlobReferencingSingleFile() {
     PsiFile ref = createPsiFile("java/com/google/Test.java");
     BuildFile file = createBuildFile("java/com/google/BUILD", "glob(['**/*.java'])");
@@ -42,6 +47,7 @@
     assertThat(references).containsExactly(ref);
   }
 
+  @Test
   public void testSimpleGlobReferencingSingleFile2() {
     PsiFile ref = createPsiFile("java/com/google/Test.java");
     BuildFile file = createBuildFile("java/com/google/BUILD", "glob(['*.java'])");
@@ -52,6 +58,7 @@
     assertThat(references).containsExactly(ref);
   }
 
+  @Test
   public void testSimpleGlobReferencingSingleFile3() {
     PsiFile ref = createPsiFile("java/com/google/Test.java");
     BuildFile file = createBuildFile("java/com/google/BUILD", "glob(['T*t.java'])");
@@ -62,6 +69,7 @@
     assertThat(references).containsExactly(ref);
   }
 
+  @Test
   public void testGlobReferencingMultipleFiles() {
     PsiFile ref1 = createPsiFile("java/com/google/Test.java");
     PsiFile ref2 = createPsiFile("java/com/google/Foo.java");
@@ -73,6 +81,7 @@
     assertThat(references).containsExactly(ref1, ref2);
   }
 
+  @Test
   public void testFindsSubDirectories() {
     PsiFile ref1 = createPsiFile("java/com/google/test/Test.java");
     PsiFile ref2 = createPsiFile("java/com/google/Foo.java");
@@ -84,8 +93,9 @@
     assertThat(references).containsExactly(ref1, ref2);
   }
 
+  @Test
   public void testGlobWithExcludes() {
-    PsiFile test = createPsiFile("java/com/google/tests/Test.java");
+    createPsiFile("java/com/google/tests/Test.java");
     PsiFile foo = createPsiFile("java/com/google/Foo.java");
     BuildFile file =
         createBuildFile(
@@ -98,6 +108,7 @@
     assertThat(references).containsExactly(foo);
   }
 
+  @Test
   public void testIncludeDirectories() {
     createDirectory("java/com/google/tests");
     PsiFile test = createPsiFile("java/com/google/tests/Test.java");
@@ -113,6 +124,7 @@
     assertThat(references).containsExactly(foo, test, test.getParent());
   }
 
+  @Test
   public void testExcludeDirectories() {
     createDirectory("java/com/google/tests");
     PsiFile test = createPsiFile("java/com/google/tests/Test.java");
@@ -127,9 +139,10 @@
     assertThat(references).containsExactly(foo, test);
   }
 
+  @Test
   public void testFilesInSubpackagesExcluded() {
     BuildFile pkg = createBuildFile("java/com/google/BUILD", "glob(['**/*.java'])");
-    BuildFile subPkg = createBuildFile("java/com/google/other/BUILD");
+    createBuildFile("java/com/google/other/BUILD");
     createFile("java/com/google/other/Other.java");
 
     GlobExpression glob = PsiUtils.findFirstChildOfClassRecursive(pkg, GlobExpression.class);
diff --git a/base/tests/integrationtests/com/google/idea/blaze/base/lang/buildfile/references/KeywordReferenceTest.java b/base/tests/integrationtests/com/google/idea/blaze/base/lang/buildfile/references/KeywordReferenceTest.java
index 310375f..fd19915 100644
--- a/base/tests/integrationtests/com/google/idea/blaze/base/lang/buildfile/references/KeywordReferenceTest.java
+++ b/base/tests/integrationtests/com/google/idea/blaze/base/lang/buildfile/references/KeywordReferenceTest.java
@@ -24,10 +24,15 @@
 import com.google.idea.blaze.base.lang.buildfile.psi.FunctionStatement;
 import com.google.idea.blaze.base.lang.buildfile.psi.Parameter;
 import com.google.idea.blaze.base.lang.buildfile.psi.ParameterList;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
 
 /** Tests that keyword references are correctly resolved. */
+@RunWith(JUnit4.class)
 public class KeywordReferenceTest extends BuildFileIntegrationTestCase {
 
+  @Test
   public void testPlainKeywordReference() {
     BuildFile file =
         createBuildFile(
@@ -46,6 +51,7 @@
         .isEqualTo(params.findParameterByName("deps"));
   }
 
+  @Test
   public void testKwargsReference() {
     BuildFile file =
         createBuildFile(
diff --git a/base/tests/integrationtests/com/google/idea/blaze/base/lang/buildfile/references/LabelReferenceTest.java b/base/tests/integrationtests/com/google/idea/blaze/base/lang/buildfile/references/LabelReferenceTest.java
index e3f3e7f..2e7c417 100644
--- a/base/tests/integrationtests/com/google/idea/blaze/base/lang/buildfile/references/LabelReferenceTest.java
+++ b/base/tests/integrationtests/com/google/idea/blaze/base/lang/buildfile/references/LabelReferenceTest.java
@@ -28,10 +28,15 @@
 import com.intellij.psi.PsiFile;
 import com.intellij.psi.PsiReference;
 import java.util.List;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
 
 /** Tests that string literal references are correctly resolved. */
+@RunWith(JUnit4.class)
 public class LabelReferenceTest extends BuildFileIntegrationTestCase {
 
+  @Test
   public void testExternalFileReference() {
     BuildFile file =
         createBuildFile(
@@ -48,6 +53,7 @@
     assertThat(strings.get(1).getReferencedElement()).isEqualTo(xmlFile);
   }
 
+  @Test
   public void testLocalRuleReference() {
     BuildFile file =
         createBuildFile(
@@ -73,6 +79,7 @@
     assertThat(label.getReferencedElement()).isEqualTo(lib);
   }
 
+  @Test
   public void testTargetInAnotherPackageResolves() {
     BuildFile targetFile = createBuildFile("java/com/google/foo/BUILD", "rule(name = \"target\")");
 
@@ -89,6 +96,7 @@
     assertThat(depArgument.getValue().getReferencedElement()).isEqualTo(target);
   }
 
+  @Test
   public void testRuleNameDoesntCrossPackageBoundaries() {
     BuildFile targetFile =
         createBuildFile("java/com/google/pkg/subpkg/BUILD", "rule(name = \"target\")");
@@ -107,6 +115,7 @@
     assertThat(ref.resolve()).isEqualTo(targetFile.findRule("target"));
   }
 
+  @Test
   public void testLabelWithImplicitRuleName() {
     BuildFile targetFile = createBuildFile("java/com/google/foo/BUILD", "rule(name = \"foo\")");
 
@@ -122,6 +131,7 @@
     assertThat(depArgument.getValue().getReferencedElement()).isEqualTo(target);
   }
 
+  @Test
   public void testAbsoluteLabelInSkylarkExtension() {
     BuildFile targetFile = createBuildFile("java/com/google/foo/BUILD", "rule(name = \"foo\")");
 
@@ -136,6 +146,7 @@
     assertThat(label.getReferencedElement()).isEqualTo(target);
   }
 
+  @Test
   public void testRulePreferredOverFile() {
     BuildFile targetFile = createBuildFile("java/com/foo/BUILD", "java_library(name = 'lib')");
 
diff --git a/base/tests/integrationtests/com/google/idea/blaze/base/lang/buildfile/references/LoadedSkylarkExtensionTest.java b/base/tests/integrationtests/com/google/idea/blaze/base/lang/buildfile/references/LoadedSkylarkExtensionTest.java
index 7152ec1..c30e57d 100644
--- a/base/tests/integrationtests/com/google/idea/blaze/base/lang/buildfile/references/LoadedSkylarkExtensionTest.java
+++ b/base/tests/integrationtests/com/google/idea/blaze/base/lang/buildfile/references/LoadedSkylarkExtensionTest.java
@@ -22,10 +22,15 @@
 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 org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
 
 /** Tests that funcall references and load statement contents are correctly resolved. */
+@RunWith(JUnit4.class)
 public class LoadedSkylarkExtensionTest extends BuildFileIntegrationTestCase {
 
+  @Test
   public void testStandardLoadReference() {
     BuildFile extFile =
         createBuildFile("java/com/google/build_defs.bzl", "def function(name, deps)");
@@ -66,6 +71,7 @@
   //  assertThat(load.getImportPsiElement().getReferencedElement()).isEqualTo(extFile);
   //}
 
+  @Test
   public void testPackageLocalImportLabelFormat() {
     BuildFile extFile =
         createBuildFile("java/com/google/tools/build_defs.bzl", "def function(name, deps)");
@@ -78,6 +84,7 @@
     assertThat(load.getImportPsiElement().getReferencedElement()).isEqualTo(extFile);
   }
 
+  @Test
   public void testMultipleImportedFunctions() {
     BuildFile extFile =
         createBuildFile(
@@ -100,6 +107,7 @@
     assertThat(load.getImportedFunctionReferences()).isEqualTo(functions);
   }
 
+  @Test
   public void testFuncallReference() {
     BuildFile extFile =
         createBuildFile("java/com/google/tools/build_defs.bzl", "def function(name, deps)");
@@ -122,6 +130,7 @@
 
   // relative paths in skylark extensions which lie in subdirectories
   // are relative to the parent blaze package directory
+  @Test
   public void testRelativePathInSubdirectory() {
     createFile("java/com/google/BUILD");
     BuildFile referencedFile =
diff --git a/base/tests/integrationtests/com/google/idea/blaze/base/lang/buildfile/references/LocalReferenceTest.java b/base/tests/integrationtests/com/google/idea/blaze/base/lang/buildfile/references/LocalReferenceTest.java
index 7bff52c..efa232d 100644
--- a/base/tests/integrationtests/com/google/idea/blaze/base/lang/buildfile/references/LocalReferenceTest.java
+++ b/base/tests/integrationtests/com/google/idea/blaze/base/lang/buildfile/references/LocalReferenceTest.java
@@ -24,12 +24,17 @@
 import com.google.idea.blaze.base.lang.buildfile.psi.ReferenceExpression;
 import com.google.idea.blaze.base.lang.buildfile.psi.TargetExpression;
 import com.intellij.psi.PsiElement;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
 
 /**
  * Tests that local references (to TargetExpressions within a given file) are correctly resolved.
  */
+@RunWith(JUnit4.class)
 public class LocalReferenceTest extends BuildFileIntegrationTestCase {
 
+  @Test
   public void testCreatesReference() {
     BuildFile file = createBuildFile("java/com/google/BUILD", "a = 1", "c = a");
 
@@ -41,6 +46,7 @@
     assertThat(ref.getReference()).isInstanceOf(LocalReference.class);
   }
 
+  @Test
   public void testReferenceResolves() {
     BuildFile file = createBuildFile("java/com/google/BUILD", "a = 1", "c = a");
 
@@ -51,6 +57,7 @@
     assertThat(referencedElement).isEqualTo(stmts[0].getLeftHandSideExpression());
   }
 
+  @Test
   public void testTargetInOuterScope() {
     BuildFile file = createBuildFile("java/com/google/BUILD", "a = 1", "function(c = a)");
 
@@ -62,6 +69,7 @@
     assertThat(ref.getReferencedElement()).isEqualTo(target);
   }
 
+  @Test
   public void testReferenceInsideFuncallExpression() {
     BuildFile file = createBuildFile("java/com/google/BUILD", "a = 1", "a.function(c)");
 
diff --git a/base/tests/integrationtests/com/google/idea/blaze/base/lang/buildfile/references/PackageReferenceTest.java b/base/tests/integrationtests/com/google/idea/blaze/base/lang/buildfile/references/PackageReferenceTest.java
index 47d1afc..389935f 100644
--- a/base/tests/integrationtests/com/google/idea/blaze/base/lang/buildfile/references/PackageReferenceTest.java
+++ b/base/tests/integrationtests/com/google/idea/blaze/base/lang/buildfile/references/PackageReferenceTest.java
@@ -24,10 +24,15 @@
 import com.google.idea.blaze.base.lang.buildfile.psi.StringLiteral;
 import com.google.idea.blaze.base.lang.buildfile.psi.util.PsiUtils;
 import com.intellij.psi.PsiReference;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
 
 /** Tests that package references in string literals are correctly resolved. */
+@RunWith(JUnit4.class)
 public class PackageReferenceTest extends BuildFileIntegrationTestCase {
 
+  @Test
   public void testDirectReferenceResolves() {
     BuildFile buildFile1 = createBuildFile("java/com/google/tools/BUILD", "# contents");
 
@@ -46,6 +51,7 @@
     assertThat(string.getReferencedElement()).isEqualTo(buildFile1);
   }
 
+  @Test
   public void testLabelFragmentResolves() {
     BuildFile buildFile1 =
         createBuildFile("java/com/google/tools/BUILD", "java_library(name = \"lib\")");
diff --git a/base/tests/integrationtests/com/google/idea/blaze/base/lang/buildfile/search/BlazePackageTest.java b/base/tests/integrationtests/com/google/idea/blaze/base/lang/buildfile/search/BlazePackageTest.java
index 567ad1e..5ad3bd2 100644
--- a/base/tests/integrationtests/com/google/idea/blaze/base/lang/buildfile/search/BlazePackageTest.java
+++ b/base/tests/integrationtests/com/google/idea/blaze/base/lang/buildfile/search/BlazePackageTest.java
@@ -20,10 +20,15 @@
 import com.google.idea.blaze.base.lang.buildfile.BuildFileIntegrationTestCase;
 import com.google.idea.blaze.base.lang.buildfile.psi.BuildFile;
 import com.intellij.psi.PsiFile;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
 
 /** Tests for BlazePackage */
+@RunWith(JUnit4.class)
 public class BlazePackageTest extends BuildFileIntegrationTestCase {
 
+  @Test
   public void testFindPackage() {
     BuildFile packageFile = createBuildFile("java/com/google/BUILD");
     PsiFile subDirFile = createPsiFile("java/com/google/tools/test.txt");
@@ -32,32 +37,35 @@
     assertThat(blazePackage.buildFile).isEqualTo(packageFile);
   }
 
+  @Test
   public void testScopeDoesntCrossPackageBoundary() {
     BuildFile pkg = createBuildFile("java/com/google/BUILD");
     BuildFile subpkg = createBuildFile("java/com/google/other/BUILD");
 
     BlazePackage blazePackage = BlazePackage.getContainingPackage(pkg);
     assertThat(blazePackage.buildFile).isEqualTo(pkg);
-    assertFalse(blazePackage.getSearchScope(false).contains(subpkg.getVirtualFile()));
+    assertThat(blazePackage.getSearchScope(false).contains(subpkg.getVirtualFile())).isFalse();
   }
 
+  @Test
   public void testScopeIncludesSubdirectoriesWhichAreNotBlazePackages() {
     BuildFile pkg = createBuildFile("java/com/google/BUILD");
-    BuildFile subpkg = createBuildFile("java/com/google/foo/bar/BUILD");
+    createBuildFile("java/com/google/foo/bar/BUILD");
     PsiFile subDirFile = createPsiFile("java/com/google/foo/test.txt");
 
     BlazePackage blazePackage = BlazePackage.getContainingPackage(subDirFile);
     assertThat(blazePackage.buildFile).isEqualTo(pkg);
-    assertTrue(blazePackage.getSearchScope(false).contains(subDirFile.getVirtualFile()));
+    assertThat(blazePackage.getSearchScope(false).contains(subDirFile.getVirtualFile())).isTrue();
   }
 
+  @Test
   public void testScopeLimitedToBlazeFiles() {
     BuildFile pkg = createBuildFile("java/com/google/BUILD");
-    BuildFile subpkg = createBuildFile("java/com/google/foo/bar/BUILD");
+    createBuildFile("java/com/google/foo/bar/BUILD");
     PsiFile subDirFile = createPsiFile("java/com/google/foo/test.txt");
 
     BlazePackage blazePackage = BlazePackage.getContainingPackage(subDirFile);
     assertThat(blazePackage.buildFile).isEqualTo(pkg);
-    assertFalse(blazePackage.getSearchScope(true).contains(subDirFile.getVirtualFile()));
+    assertThat(blazePackage.getSearchScope(true).contains(subDirFile.getVirtualFile())).isFalse();
   }
 }
diff --git a/base/tests/integrationtests/com/google/idea/blaze/base/lang/buildfile/search/GlobalWordIndexTest.java b/base/tests/integrationtests/com/google/idea/blaze/base/lang/buildfile/search/GlobalWordIndexTest.java
index 2eb828c..4fdaf86 100644
--- a/base/tests/integrationtests/com/google/idea/blaze/base/lang/buildfile/search/GlobalWordIndexTest.java
+++ b/base/tests/integrationtests/com/google/idea/blaze/base/lang/buildfile/search/GlobalWordIndexTest.java
@@ -22,18 +22,25 @@
 import com.intellij.psi.search.UsageSearchContext;
 import java.util.Arrays;
 import org.intellij.lang.annotations.MagicConstant;
+import org.junit.Assert;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
 
 /**
  * Test the WordScanner indexes keywords in the way we expect.<br>
  * This is vital for navigation, refactoring, highlighting etc.
  */
+@RunWith(JUnit4.class)
 public class GlobalWordIndexTest extends BuildFileIntegrationTestCase {
 
+  @Test
   public void testWordsInComments() {
     VirtualFile file = createFile("java/com/google/BUILD", "# words in comments");
     assertContainsWords(file, UsageSearchContext.IN_COMMENTS, "words", "in", "comments");
   }
 
+  @Test
   public void testWordsInStrings() {
     VirtualFile file =
         createFile(
@@ -50,6 +57,7 @@
         "name_without_spaces");
   }
 
+  @Test
   public void testWordsInCode() {
     VirtualFile file =
         createFile(
@@ -73,7 +81,7 @@
               .getVirtualFilesWithWord(
                   word, occurenceMask, GlobalSearchScope.fileScope(getProject(), file), true);
       if (!Arrays.asList(files).contains(file)) {
-        fail(String.format("Word '%s' not found in file '%s'", word, file));
+        Assert.fail(String.format("Word '%s' not found in file '%s'", word, file));
       }
     }
   }
diff --git a/base/tests/integrationtests/com/google/idea/blaze/base/lang/buildfile/validation/GlobValidationTest.java b/base/tests/integrationtests/com/google/idea/blaze/base/lang/buildfile/validation/GlobValidationTest.java
index 3ba00e5..2715bbe 100644
--- a/base/tests/integrationtests/com/google/idea/blaze/base/lang/buildfile/validation/GlobValidationTest.java
+++ b/base/tests/integrationtests/com/google/idea/blaze/base/lang/buildfile/validation/GlobValidationTest.java
@@ -28,22 +28,29 @@
 import com.intellij.psi.PsiFile;
 import java.util.List;
 import java.util.stream.Collectors;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
 
 /** Tests glob validation. */
+@RunWith(JUnit4.class)
 public class GlobValidationTest extends BuildFileIntegrationTestCase {
 
+  @Test
   public void testNormalGlob() {
     BuildFile file = createBuildFile("java/com/google/BUILD", "glob(['**/*.java'])");
 
     assertNoErrors(file);
   }
 
+  @Test
   public void testNamedIncludeArgument() {
     BuildFile file = createBuildFile("java/com/google/BUILD", "glob(include = ['**/*.java'])");
 
     assertNoErrors(file);
   }
 
+  @Test
   public void testAllArguments() {
     BuildFile file =
         createBuildFile(
@@ -53,18 +60,21 @@
     assertNoErrors(file);
   }
 
+  @Test
   public void testEmptyExcludeList() {
     BuildFile file = createBuildFile("java/com/google/BUILD", "glob(['**/*.java'], exclude = [])");
 
     assertNoErrors(file);
   }
 
+  @Test
   public void testNoIncludesError() {
     BuildFile file = createBuildFile("java/com/google/BUILD", "glob(exclude = ['BUILD'])");
 
     assertHasError(file, "Glob expression must contain at least one included string");
   }
 
+  @Test
   public void testSingletonExcludeArgumentError() {
     BuildFile file =
         createBuildFile("java/com/google/BUILD", "glob(['**/*.java'], exclude = 'BUILD')");
@@ -72,12 +82,14 @@
     assertHasError(file, "Glob parameter 'exclude' must be a list of strings");
   }
 
+  @Test
   public void testSingletonIncludeArgumentError() {
     BuildFile file = createBuildFile("java/com/google/BUILD", "glob(include = '**/*.java')");
 
     assertHasError(file, "Glob parameter 'include' must be a list of strings");
   }
 
+  @Test
   public void testInvalidExcludeDirectoriesValue() {
     BuildFile file =
         createBuildFile(
@@ -87,6 +99,7 @@
     assertHasError(file, "exclude_directories parameter to glob must be 0 or 1");
   }
 
+  @Test
   public void testUnrecognizedArgumentError() {
     BuildFile file =
         createBuildFile(
@@ -95,12 +108,14 @@
     assertHasError(file, "Unrecognized glob argument");
   }
 
+  @Test
   public void testInvalidListArgumentValue() {
     BuildFile file = createBuildFile("java/com/google/BUILD", "glob(include = foo)");
 
     assertHasError(file, "Glob parameter 'include' must be a list of strings");
   }
 
+  @Test
   public void testLocalVariableReference() {
     BuildFile file =
         createBuildFile("java/com/google/BUILD", "foo = ['*.java']", "glob(include = foo)");
@@ -108,8 +123,9 @@
     assertNoErrors(file);
   }
 
+  @Test
   public void testLoadedVariableReference() {
-    BuildFile ext = createBuildFile("java/com/foo/vars.bzl", "LIST_VAR = ['*']");
+    createBuildFile("java/com/foo/vars.bzl", "LIST_VAR = ['*']");
     BuildFile file =
         createBuildFile(
             "java/com/google/BUILD",
@@ -119,8 +135,9 @@
     assertNoErrors(file);
   }
 
+  @Test
   public void testInvalidLoadedVariableReference() {
-    BuildFile ext = createBuildFile("java/com/foo/vars.bzl", "LIST_VAR = ['*']", "def function()");
+    createBuildFile("java/com/foo/vars.bzl", "LIST_VAR = ['*']", "def function()");
     BuildFile file =
         createBuildFile(
             "java/com/google/BUILD",
@@ -130,18 +147,21 @@
     assertHasError(file, "Glob parameter 'include' must be a list of strings");
   }
 
+  @Test
   public void testUnresolvedReferenceExpression() {
     BuildFile file = createBuildFile("java/com/google/BUILD", "glob(include = ref)");
 
     assertHasError(file, "Glob parameter 'include' must be a list of strings");
   }
 
+  @Test
   public void testPossibleListExpressionFuncallExpression() {
     BuildFile file = createBuildFile("java/com/google/BUILD", "glob(include = fn.list)");
 
     assertNoErrors(file);
   }
 
+  @Test
   public void testPossibleListExpressionParameter() {
     BuildFile file =
         createBuildFile(
@@ -150,6 +170,7 @@
     assertNoErrors(file);
   }
 
+  @Test
   public void testNestedGlobs() {
     // blaze accepts nested globs
     BuildFile file = createBuildFile("java/com/google/BUILD", "glob(glob(['*.java']))");
@@ -157,6 +178,7 @@
     assertNoErrors(file);
   }
 
+  @Test
   public void testKnownInvalidResolvedListExpression() {
     BuildFile file =
         createBuildFile("java/com/google/BUILD", "bool_literal = True", "glob(bool_literal)");
@@ -164,6 +186,7 @@
     assertHasError(file, "Glob parameter 'include' must be a list of strings");
   }
 
+  @Test
   public void testKnownInvalidResolvedString() {
     BuildFile file =
         createBuildFile("java/com/google/BUILD", "bool_literal = True", "glob([bool_literal])");
@@ -171,6 +194,7 @@
     assertHasError(file, "Glob parameter 'include' must be a list of strings");
   }
 
+  @Test
   public void testPossibleStringLiteralIfStatement() {
     BuildFile file =
         createBuildFile("java/com/google/BUILD", "glob(include = ['*.java', if test : a else b])");
@@ -179,6 +203,7 @@
     assertNoErrors(file);
   }
 
+  @Test
   public void testPossibleStringLiteralParameter() {
     BuildFile file =
         createBuildFile(
@@ -206,10 +231,8 @@
 
   private List<Annotation> validateFile(BuildFile file) {
     GlobErrorAnnotator annotator = createAnnotator(file);
-    for (GlobExpression glob :
-        PsiUtils.findAllChildrenOfClassRecursive(file, GlobExpression.class)) {
-      annotator.visitGlobExpression(glob);
-    }
+    PsiUtils.findAllChildrenOfClassRecursive(file, GlobExpression.class)
+        .forEach(annotator::visitGlobExpression);
     return annotationHolder;
   }
 
diff --git a/base/tests/integrationtests/com/google/idea/blaze/base/lang/projectview/ProjectViewCompletionTest.java b/base/tests/integrationtests/com/google/idea/blaze/base/lang/projectview/ProjectViewCompletionTest.java
index 4e629f8..3628b85 100644
--- a/base/tests/integrationtests/com/google/idea/blaze/base/lang/projectview/ProjectViewCompletionTest.java
+++ b/base/tests/integrationtests/com/google/idea/blaze/base/lang/projectview/ProjectViewCompletionTest.java
@@ -27,8 +27,13 @@
 import com.intellij.psi.PsiFile;
 import java.util.Arrays;
 import java.util.stream.Collectors;
+import org.junit.Assert;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
 
 /** Tests auto-complete in project view files */
+@RunWith(JUnit4.class)
 public class ProjectViewCompletionTest extends ProjectViewIntegrationTestCase {
 
   private PsiFile setInput(String... fileContents) {
@@ -36,10 +41,11 @@
   }
 
   private void assertResult(String... resultingFileContents) {
-    String s = testFixture.getFile().getText();
+    testFixture.getFile().getText();
     testFixture.checkResult(Joiner.on("\n").join(resultingFileContents));
   }
 
+  @Test
   public void testSectionTypeKeywords() {
     setInput("<caret>");
     String[] keywords = getCompletionItemsAsStrings();
@@ -53,12 +59,14 @@
                 .collect(Collectors.toList()));
   }
 
+  @Test
   public void testColonAndNewLineAndIndentInsertedAfterListSection() {
     setInput("direc<caret>");
     assertThat(completeIfUnique()).isTrue();
     assertResult("directories:", "  <caret>");
   }
 
+  @Test
   public void testWhitespaceDividerInsertedAfterScalarSection() {
     setInput("impo<caret>");
 
@@ -71,32 +79,36 @@
     assertResult("import <caret>");
   }
 
+  @Test
   public void testColonDividerAndSpaceInsertedAfterScalarSection() {
     setInput("works<caret>");
     assertThat(completeIfUnique()).isTrue();
     assertResult("workspace_type: <caret>");
   }
 
+  @Test
   public void testNoKeywordCompletionInListItem() {
     setInput("directories:", "  <caret>");
 
     String[] completionItems = getCompletionItemsAsStrings();
     if (completionItems == null) {
-      fail("Spurious completion. New file contents: " + testFixture.getFile().getText());
+      Assert.fail("Spurious completion. New file contents: " + testFixture.getFile().getText());
     }
     assertThat(completionItems).isEmpty();
   }
 
+  @Test
   public void testNoKeywordCompletionAfterKeyword() {
     setInput("import <caret>");
 
     String[] completionItems = getCompletionItemsAsStrings();
     if (completionItems == null) {
-      fail("Spurious completion. New file contents: " + testFixture.getFile().getText());
+      Assert.fail("Spurious completion. New file contents: " + testFixture.getFile().getText());
     }
     assertThat(completionItems).isEmpty();
   }
 
+  @Test
   public void testWorkspaceTypeCompletion() {
     setInput("workspace_type: <caret>");
 
@@ -110,6 +122,7 @@
                 .collect(Collectors.toList()));
   }
 
+  @Test
   public void testAdditionalLanguagesCompletion() {
     setInput("additional_languages:", "  <caret>");
 
@@ -123,6 +136,7 @@
                 .collect(Collectors.toList()));
   }
 
+  @Test
   public void testUniqueDirectoryCompleted() {
     setInput("import <caret>");
 
@@ -133,6 +147,7 @@
     assertResult("import java<caret>");
   }
 
+  @Test
   public void testUniqueMultiSegmentDirectoryCompleted() {
     setInput("import <caret>");
 
@@ -143,6 +158,7 @@
     assertResult("import java/com/google<caret>");
   }
 
+  @Test
   public void testNonDirectoriesIgnored() {
     setInput("import <caret>");
 
@@ -154,6 +170,7 @@
     assertResult("import java/com/google<caret>");
   }
 
+  @Test
   public void testMultipleDirectoryOptions() {
     createDirectory("foo");
     createDirectory("bar");
@@ -173,6 +190,7 @@
     assertResult("targets:", "  //ostrich<caret>");
   }
 
+  @Test
   public void testRuleCompletion() {
     createFile("BUILD", "java_library(name = 'lib')");
 
diff --git a/base/tests/integrationtests/com/google/idea/blaze/base/lang/projectview/ProjectViewIntegrationTestCase.java b/base/tests/integrationtests/com/google/idea/blaze/base/lang/projectview/ProjectViewIntegrationTestCase.java
index 151d384..96b5f8a 100644
--- a/base/tests/integrationtests/com/google/idea/blaze/base/lang/projectview/ProjectViewIntegrationTestCase.java
+++ b/base/tests/integrationtests/com/google/idea/blaze/base/lang/projectview/ProjectViewIntegrationTestCase.java
@@ -18,18 +18,22 @@
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableMap;
 import com.google.idea.blaze.base.BlazeIntegrationTestCase;
+import com.google.idea.blaze.base.ideinfo.RuleMap;
 import com.google.idea.blaze.base.model.BlazeProjectData;
-import com.google.idea.blaze.base.model.RuleMap;
 import com.google.idea.blaze.base.model.primitives.ExecutionRootPath;
+import com.google.idea.blaze.base.sync.workspace.ArtifactLocationDecoder;
+import com.google.idea.blaze.base.sync.workspace.ArtifactLocationDecoderImpl;
 import com.google.idea.blaze.base.sync.workspace.BlazeRoots;
 import com.google.idea.blaze.base.sync.workspace.WorkingSet;
+import com.google.idea.blaze.base.sync.workspace.WorkspacePathResolver;
 import com.google.idea.blaze.base.sync.workspace.WorkspacePathResolverImpl;
+import org.junit.Before;
 
 /** Project view file specific integration test base */
 public abstract class ProjectViewIntegrationTestCase extends BlazeIntegrationTestCase {
 
-  @Override
-  protected void doSetup() {
+  @Before
+  public final void doSetup() {
     mockBlazeProjectDataManager(getMockBlazeProjectData());
   }
 
@@ -40,12 +44,17 @@
             ImmutableList.of(workspaceRoot.directory()),
             new ExecutionRootPath("out/crosstool/bin"),
             new ExecutionRootPath("out/crosstool/gen"));
+    WorkspacePathResolver workspacePathResolver =
+        new WorkspacePathResolverImpl(workspaceRoot, fakeRoots);
+    ArtifactLocationDecoder artifactLocationDecoder =
+        new ArtifactLocationDecoderImpl(fakeRoots, workspacePathResolver);
     return new BlazeProjectData(
         0,
         new RuleMap(ImmutableMap.of()),
         fakeRoots,
         new WorkingSet(ImmutableList.of(), ImmutableList.of(), ImmutableList.of()),
-        new WorkspacePathResolverImpl(workspaceRoot, fakeRoots),
+        workspacePathResolver,
+        artifactLocationDecoder,
         null,
         null,
         null,
diff --git a/base/tests/integrationtests/com/google/idea/blaze/base/lang/projectview/ProjectViewParserIntegrationTest.java b/base/tests/integrationtests/com/google/idea/blaze/base/lang/projectview/ProjectViewParserIntegrationTest.java
index 68cf418..4fee948 100644
--- a/base/tests/integrationtests/com/google/idea/blaze/base/lang/projectview/ProjectViewParserIntegrationTest.java
+++ b/base/tests/integrationtests/com/google/idea/blaze/base/lang/projectview/ProjectViewParserIntegrationTest.java
@@ -28,18 +28,23 @@
 import java.util.Arrays;
 import java.util.List;
 import java.util.stream.Collectors;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
 
 /** Tests for the project view file parser */
+@RunWith(JUnit4.class)
 public class ProjectViewParserIntegrationTest extends ProjectViewIntegrationTestCase {
 
   private final List<String> errors = Lists.newArrayList();
 
-  @Override
-  protected void doSetup() {
+  @Before
+  public final void before() {
     errors.clear();
-    super.doSetup();
   }
 
+  @Test
   public void testStandardFile() {
     assertThat(
             parse(
@@ -57,6 +62,7 @@
     assertNoErrors();
   }
 
+  @Test
   public void testIncludeScalarSections() {
     assertThat(
             parse(
@@ -79,6 +85,7 @@
     assertNoErrors();
   }
 
+  @Test
   public void testUnrecognizedKeyword() {
     parse("impart java/com/google/work/.blazeproject", "", "workspace_trype: intellij_plugin");
 
diff --git a/base/tests/integrationtests/com/google/idea/blaze/base/lang/projectview/lexer/ProjectViewLexerTest.java b/base/tests/integrationtests/com/google/idea/blaze/base/lang/projectview/lexer/ProjectViewLexerTest.java
index 610aef3..8821c02 100644
--- a/base/tests/integrationtests/com/google/idea/blaze/base/lang/projectview/lexer/ProjectViewLexerTest.java
+++ b/base/tests/integrationtests/com/google/idea/blaze/base/lang/projectview/lexer/ProjectViewLexerTest.java
@@ -20,10 +20,15 @@
 import com.google.common.base.Joiner;
 import com.google.idea.blaze.base.lang.projectview.ProjectViewIntegrationTestCase;
 import com.google.idea.blaze.base.lang.projectview.lexer.ProjectViewLexerBase.Token;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
 
 /** Tests for the project view file lexer */
+@RunWith(JUnit4.class)
 public class ProjectViewLexerTest extends ProjectViewIntegrationTestCase {
 
+  @Test
   public void testStandardCase() {
     String result =
         tokenize(
@@ -47,6 +52,7 @@
                     "indent identifier : identifier"));
   }
 
+  @Test
   public void testIncludeScalarSections() {
     String result =
         tokenize(
@@ -72,6 +78,7 @@
                     "indent identifier"));
   }
 
+  @Test
   public void testUnrecognizedKeyword() {
     String result =
         tokenize(
diff --git a/base/tests/integrationtests/com/google/idea/blaze/base/run/BlazeCommandRunConfigurationGenericHandlerIntegrationTest.java b/base/tests/integrationtests/com/google/idea/blaze/base/run/BlazeCommandRunConfigurationGenericHandlerIntegrationTest.java
index 1fe63b7..eac7ae9 100644
--- a/base/tests/integrationtests/com/google/idea/blaze/base/run/BlazeCommandRunConfigurationGenericHandlerIntegrationTest.java
+++ b/base/tests/integrationtests/com/google/idea/blaze/base/run/BlazeCommandRunConfigurationGenericHandlerIntegrationTest.java
@@ -21,24 +21,32 @@
 import com.google.common.collect.ImmutableMap;
 import com.google.idea.blaze.base.BlazeIntegrationTestCase;
 import com.google.idea.blaze.base.command.BlazeCommandName;
+import com.google.idea.blaze.base.ideinfo.RuleMap;
 import com.google.idea.blaze.base.model.BlazeProjectData;
-import com.google.idea.blaze.base.model.RuleMap;
 import com.google.idea.blaze.base.model.primitives.ExecutionRootPath;
 import com.google.idea.blaze.base.model.primitives.TargetExpression;
 import com.google.idea.blaze.base.run.BlazeCommandRunConfiguration.BlazeCommandRunConfigurationSettingsEditor;
 import com.google.idea.blaze.base.run.confighandler.BlazeCommandGenericRunConfigurationHandler;
-import com.google.idea.blaze.base.run.confighandler.BlazeUnknownRunConfigurationHandler;
+import com.google.idea.blaze.base.run.state.BlazeCommandRunConfigurationCommonState;
+import com.google.idea.blaze.base.sync.workspace.ArtifactLocationDecoder;
+import com.google.idea.blaze.base.sync.workspace.ArtifactLocationDecoderImpl;
 import com.google.idea.blaze.base.sync.workspace.BlazeRoots;
 import com.google.idea.blaze.base.sync.workspace.WorkingSet;
+import com.google.idea.blaze.base.sync.workspace.WorkspacePathResolver;
 import com.google.idea.blaze.base.sync.workspace.WorkspacePathResolverImpl;
 import com.intellij.openapi.options.ConfigurationException;
 import com.intellij.openapi.util.Disposer;
 import org.jdom.Element;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
 
 /**
- * Integration tests for {@link BlazeCommandRunConfiguration} with {@link
- * BlazeCommandGenericRunConfigurationHandler} and {@link BlazeUnknownRunConfigurationHandler}.
+ * Integration tests for {@link BlazeCommandRunConfiguration} with a {@link
+ * BlazeCommandGenericRunConfigurationHandler}.
  */
+@RunWith(JUnit4.class)
 public class BlazeCommandRunConfigurationGenericHandlerIntegrationTest
     extends BlazeIntegrationTestCase {
   private static final BlazeCommandName COMMAND = BlazeCommandName.fromString("command");
@@ -46,9 +54,8 @@
   private BlazeCommandRunConfigurationType type;
   private BlazeCommandRunConfiguration configuration;
 
-  @Override
-  protected void doSetup() throws Exception {
-    super.doSetup();
+  @Before
+  public final void doSetup() throws Exception {
     // Without BlazeProjectData, the configuration editor is always disabled.
     mockBlazeProjectDataManager(getMockBlazeProjectData());
     type = BlazeCommandRunConfigurationType.getInstance();
@@ -62,44 +69,54 @@
             ImmutableList.of(workspaceRoot.directory()),
             new ExecutionRootPath("out/crosstool/bin"),
             new ExecutionRootPath("out/crosstool/gen"));
+    WorkspacePathResolver workspacePathResolver =
+        new WorkspacePathResolverImpl(workspaceRoot, fakeRoots);
+    ArtifactLocationDecoder artifactLocationDecoder =
+        new ArtifactLocationDecoderImpl(fakeRoots, workspacePathResolver);
     return new BlazeProjectData(
         0,
         new RuleMap(ImmutableMap.of()),
         fakeRoots,
         new WorkingSet(ImmutableList.of(), ImmutableList.of(), ImmutableList.of()),
-        new WorkspacePathResolverImpl(workspaceRoot, fakeRoots),
+        workspacePathResolver,
+        artifactLocationDecoder,
         null,
         null,
         null,
         null);
   }
 
-  public void testNewConfigurationHasUnknownHandler() {
-    assertThat(configuration.getHandler()).isInstanceOf(BlazeUnknownRunConfigurationHandler.class);
+  @Test
+  public void testNewConfigurationHasGenericHandler() {
+    assertThat(configuration.getHandler())
+        .isInstanceOf(BlazeCommandGenericRunConfigurationHandler.class);
   }
 
+  @Test
   public void testSetTargetNullMakesGenericHandler() {
     configuration.setTarget(null);
     assertThat(configuration.getHandler())
         .isInstanceOf(BlazeCommandGenericRunConfigurationHandler.class);
   }
 
+  @Test
   public void testTargetExpressionMakesGenericHandler() {
     configuration.setTarget(TargetExpression.fromString("//..."));
     assertThat(configuration.getHandler())
         .isInstanceOf(BlazeCommandGenericRunConfigurationHandler.class);
   }
 
+  @Test
   public void testReadAndWriteMatches() throws Exception {
     TargetExpression targetExpression = TargetExpression.fromString("//...");
     configuration.setTarget(targetExpression);
 
-    BlazeCommandGenericRunConfigurationHandler handler =
-        (BlazeCommandGenericRunConfigurationHandler) configuration.getHandler();
-    handler.setCommand(COMMAND);
-    handler.setBlazeFlags(ImmutableList.of("--flag1", "--flag2"));
-    handler.setExeFlags(ImmutableList.of("--exeFlag1"));
-    handler.setBlazeBinary("/usr/bin/blaze");
+    BlazeCommandRunConfigurationCommonState state =
+        (BlazeCommandRunConfigurationCommonState) configuration.getHandler().getState();
+    state.setCommand(COMMAND);
+    state.setBlazeFlags(ImmutableList.of("--flag1", "--flag2"));
+    state.setExeFlags(ImmutableList.of("--exeFlag1"));
+    state.setBlazeBinary("/usr/bin/blaze");
 
     Element element = new Element("test");
     configuration.writeExternal(element);
@@ -111,14 +128,15 @@
     assertThat(readConfiguration.getHandler())
         .isInstanceOf(BlazeCommandGenericRunConfigurationHandler.class);
 
-    BlazeCommandGenericRunConfigurationHandler readHandler =
-        (BlazeCommandGenericRunConfigurationHandler) readConfiguration.getHandler();
-    assertThat(readHandler.getCommand()).isEqualTo(COMMAND);
-    assertThat(readHandler.getAllBlazeFlags()).containsExactly("--flag1", "--flag2").inOrder();
-    assertThat(readHandler.getAllExeFlags()).containsExactly("--exeFlag1");
-    assertThat(readHandler.getBlazeBinary()).isEqualTo("/usr/bin/blaze");
+    BlazeCommandRunConfigurationCommonState readState =
+        (BlazeCommandRunConfigurationCommonState) readConfiguration.getHandler().getState();
+    assertThat(readState.getCommand()).isEqualTo(COMMAND);
+    assertThat(readState.getBlazeFlags()).containsExactly("--flag1", "--flag2").inOrder();
+    assertThat(readState.getExeFlags()).containsExactly("--exeFlag1");
+    assertThat(readState.getBlazeBinary()).isEqualTo("/usr/bin/blaze");
   }
 
+  @Test
   public void testReadAndWriteHandlesNulls() throws Exception {
     Element element = new Element("test");
     configuration.writeExternal(element);
@@ -128,59 +146,22 @@
 
     assertThat(readConfiguration.getTarget()).isEqualTo(configuration.getTarget());
     assertThat(readConfiguration.getHandler())
-        .isInstanceOf(BlazeUnknownRunConfigurationHandler.class);
-  }
-
-  public void testEditorWithUnknownHandlerDoesNotApplyTo() throws ConfigurationException {
-    assertThat(configuration.getTarget()).isNull();
-    assertThat(configuration.getHandler()).isInstanceOf(BlazeUnknownRunConfigurationHandler.class);
-
-    BlazeCommandRunConfigurationSettingsEditor editor =
-        new BlazeCommandRunConfigurationSettingsEditor(configuration);
-    // Because the configuration's handler is BlazeUnknownRunConfigurationHandler,
-    // resetting the editor to it will leave it in the disabled state.
-    editor.resetFrom(configuration);
-
-    BlazeCommandRunConfiguration readConfiguration =
-        type.getFactory().createTemplateConfiguration(getProject());
-    TargetExpression targetExpression = TargetExpression.fromString("//...");
-    readConfiguration.setTarget(targetExpression);
-
-    BlazeCommandGenericRunConfigurationHandler readHandler =
-        (BlazeCommandGenericRunConfigurationHandler) readConfiguration.getHandler();
-    readHandler.setCommand(COMMAND);
-    readHandler.setBlazeFlags(ImmutableList.of("--flag1", "--flag2"));
-    readHandler.setExeFlags(ImmutableList.of("--exeFlag1"));
-    readHandler.setBlazeBinary("/usr/bin/blaze");
-
-    // The editor is disabled, making applyEditorTo a no-op.
-    editor.applyEditorTo(readConfiguration);
-
-    assertThat(readConfiguration.getTarget()).isEqualTo(targetExpression);
-    assertThat(readConfiguration.getHandler())
         .isInstanceOf(BlazeCommandGenericRunConfigurationHandler.class);
-
-    readHandler = (BlazeCommandGenericRunConfigurationHandler) readConfiguration.getHandler();
-    assertThat(readHandler.getCommand()).isEqualTo(COMMAND);
-    assertThat(readHandler.getAllBlazeFlags()).containsExactly("--flag1", "--flag2").inOrder();
-    assertThat(readHandler.getAllExeFlags()).containsExactly("--exeFlag1");
-    assertThat(readHandler.getBlazeBinary()).isEqualTo("/usr/bin/blaze");
-
-    Disposer.dispose(editor);
   }
 
+  @Test
   public void testEditorApplyToAndResetFromMatches() throws ConfigurationException {
     BlazeCommandRunConfigurationSettingsEditor editor =
         new BlazeCommandRunConfigurationSettingsEditor(configuration);
     TargetExpression targetExpression = TargetExpression.fromString("//...");
     configuration.setTarget(targetExpression);
 
-    BlazeCommandGenericRunConfigurationHandler handler =
-        (BlazeCommandGenericRunConfigurationHandler) configuration.getHandler();
-    handler.setCommand(COMMAND);
-    handler.setBlazeFlags(ImmutableList.of("--flag1", "--flag2"));
-    handler.setExeFlags(ImmutableList.of("--exeFlag1"));
-    handler.setBlazeBinary("/usr/bin/blaze");
+    BlazeCommandRunConfigurationCommonState state =
+        (BlazeCommandRunConfigurationCommonState) configuration.getHandler().getState();
+    state.setCommand(COMMAND);
+    state.setBlazeFlags(ImmutableList.of("--flag1", "--flag2"));
+    state.setExeFlags(ImmutableList.of("--exeFlag1"));
+    state.setBlazeBinary("/usr/bin/blaze");
 
     editor.resetFrom(configuration);
     BlazeCommandRunConfiguration readConfiguration =
@@ -191,16 +172,17 @@
     assertThat(readConfiguration.getHandler())
         .isInstanceOf(BlazeCommandGenericRunConfigurationHandler.class);
 
-    BlazeCommandGenericRunConfigurationHandler readHandler =
-        (BlazeCommandGenericRunConfigurationHandler) readConfiguration.getHandler();
-    assertThat(readHandler.getCommand()).isEqualTo(handler.getCommand());
-    assertThat(readHandler.getAllBlazeFlags()).isEqualTo(handler.getAllBlazeFlags());
-    assertThat(readHandler.getAllExeFlags()).isEqualTo(handler.getAllExeFlags());
-    assertThat(readHandler.getBlazeBinary()).isEqualTo(handler.getBlazeBinary());
+    BlazeCommandRunConfigurationCommonState readState =
+        (BlazeCommandRunConfigurationCommonState) readConfiguration.getHandler().getState();
+    assertThat(readState.getCommand()).isEqualTo(state.getCommand());
+    assertThat(readState.getBlazeFlags()).isEqualTo(state.getBlazeFlags());
+    assertThat(readState.getExeFlags()).isEqualTo(state.getExeFlags());
+    assertThat(readState.getBlazeBinary()).isEqualTo(state.getBlazeBinary());
 
     Disposer.dispose(editor);
   }
 
+  @Test
   public void testEditorApplyToAndResetFromHandlesNulls() throws ConfigurationException {
     BlazeCommandRunConfigurationSettingsEditor editor =
         new BlazeCommandRunConfigurationSettingsEditor(configuration);
@@ -210,8 +192,8 @@
     assertThat(configuration.getTarget()).isNull();
     assertThat(configuration.getHandler())
         .isInstanceOf(BlazeCommandGenericRunConfigurationHandler.class);
-    BlazeCommandGenericRunConfigurationHandler handler =
-        (BlazeCommandGenericRunConfigurationHandler) configuration.getHandler();
+    BlazeCommandRunConfigurationCommonState state =
+        (BlazeCommandRunConfigurationCommonState) configuration.getHandler().getState();
 
     editor.resetFrom(configuration);
 
@@ -220,12 +202,12 @@
     TargetExpression targetExpression = TargetExpression.fromString("//...");
     readConfiguration.setTarget(targetExpression);
 
-    BlazeCommandGenericRunConfigurationHandler readHandler =
-        (BlazeCommandGenericRunConfigurationHandler) readConfiguration.getHandler();
-    readHandler.setCommand(COMMAND);
-    readHandler.setBlazeFlags(ImmutableList.of("--flag1", "--flag2"));
-    readHandler.setExeFlags(ImmutableList.of("--exeFlag1"));
-    readHandler.setBlazeBinary("/usr/bin/blaze");
+    BlazeCommandRunConfigurationCommonState readState =
+        (BlazeCommandRunConfigurationCommonState) readConfiguration.getHandler().getState();
+    readState.setCommand(COMMAND);
+    readState.setBlazeFlags(ImmutableList.of("--flag1", "--flag2"));
+    readState.setExeFlags(ImmutableList.of("--exeFlag1"));
+    readState.setBlazeBinary("/usr/bin/blaze");
 
     editor.applyEditorTo(readConfiguration);
 
@@ -233,11 +215,11 @@
     assertThat(configuration.getHandler())
         .isInstanceOf(BlazeCommandGenericRunConfigurationHandler.class);
 
-    readHandler = (BlazeCommandGenericRunConfigurationHandler) readConfiguration.getHandler();
-    assertThat(readHandler.getCommand()).isEqualTo(handler.getCommand());
-    assertThat(readHandler.getAllBlazeFlags()).isEqualTo(handler.getAllBlazeFlags());
-    assertThat(readHandler.getAllExeFlags()).isEqualTo(handler.getAllExeFlags());
-    assertThat(readHandler.getBlazeBinary()).isEqualTo(handler.getBlazeBinary());
+    readState = (BlazeCommandRunConfigurationCommonState) readConfiguration.getHandler().getState();
+    assertThat(readState.getCommand()).isEqualTo(state.getCommand());
+    assertThat(readState.getBlazeFlags()).isEqualTo(state.getBlazeFlags());
+    assertThat(readState.getExeFlags()).isEqualTo(state.getExeFlags());
+    assertThat(readState.getBlazeBinary()).isEqualTo(state.getBlazeBinary());
 
     Disposer.dispose(editor);
   }
diff --git a/base/tests/integrationtests/com/google/idea/blaze/base/run/BlazeCommandRunConfigurationSettingsEditorTest.java b/base/tests/integrationtests/com/google/idea/blaze/base/run/BlazeCommandRunConfigurationSettingsEditorTest.java
index a0a044d..56ffef1 100644
--- a/base/tests/integrationtests/com/google/idea/blaze/base/run/BlazeCommandRunConfigurationSettingsEditorTest.java
+++ b/base/tests/integrationtests/com/google/idea/blaze/base/run/BlazeCommandRunConfigurationSettingsEditorTest.java
@@ -20,26 +20,33 @@
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableMap;
 import com.google.idea.blaze.base.BlazeIntegrationTestCase;
+import com.google.idea.blaze.base.ideinfo.RuleMap;
 import com.google.idea.blaze.base.model.BlazeProjectData;
-import com.google.idea.blaze.base.model.RuleMap;
 import com.google.idea.blaze.base.model.primitives.ExecutionRootPath;
 import com.google.idea.blaze.base.model.primitives.Label;
 import com.google.idea.blaze.base.run.BlazeCommandRunConfiguration.BlazeCommandRunConfigurationSettingsEditor;
+import com.google.idea.blaze.base.sync.workspace.ArtifactLocationDecoder;
+import com.google.idea.blaze.base.sync.workspace.ArtifactLocationDecoderImpl;
 import com.google.idea.blaze.base.sync.workspace.BlazeRoots;
 import com.google.idea.blaze.base.sync.workspace.WorkingSet;
+import com.google.idea.blaze.base.sync.workspace.WorkspacePathResolver;
 import com.google.idea.blaze.base.sync.workspace.WorkspacePathResolverImpl;
 import com.intellij.openapi.options.ConfigurationException;
 import com.intellij.openapi.util.Disposer;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
 
 /** Tests for {@link BlazeCommandRunConfiguration.BlazeCommandRunConfigurationSettingsEditor}. */
+@RunWith(JUnit4.class)
 public class BlazeCommandRunConfigurationSettingsEditorTest extends BlazeIntegrationTestCase {
 
   private BlazeCommandRunConfigurationType type;
   private BlazeCommandRunConfiguration configuration;
 
-  @Override
-  protected void doSetup() throws Exception {
-    super.doSetup();
+  @Before
+  public final void doSetup() throws Exception {
     // Without BlazeProjectData, the configuration editor is always disabled.
     mockBlazeProjectDataManager(getMockBlazeProjectData());
     type = BlazeCommandRunConfigurationType.getInstance();
@@ -53,18 +60,24 @@
             ImmutableList.of(workspaceRoot.directory()),
             new ExecutionRootPath("out/crosstool/bin"),
             new ExecutionRootPath("out/crosstool/gen"));
+    WorkspacePathResolver workspacePathResolver =
+        new WorkspacePathResolverImpl(workspaceRoot, fakeRoots);
+    ArtifactLocationDecoder artifactLocationDecoder =
+        new ArtifactLocationDecoderImpl(fakeRoots, workspacePathResolver);
     return new BlazeProjectData(
         0,
         new RuleMap(ImmutableMap.of()),
         fakeRoots,
         new WorkingSet(ImmutableList.of(), ImmutableList.of(), ImmutableList.of()),
-        new WorkspacePathResolverImpl(workspaceRoot, fakeRoots),
+        workspacePathResolver,
+        artifactLocationDecoder,
         null,
         null,
         null,
         null);
   }
 
+  @Test
   public void testEditorApplyToAndResetFromMatches() throws ConfigurationException {
     BlazeCommandRunConfigurationSettingsEditor editor =
         new BlazeCommandRunConfigurationSettingsEditor(configuration);
@@ -81,6 +94,7 @@
     Disposer.dispose(editor);
   }
 
+  @Test
   public void testEditorApplyToAndResetFromHandlesNulls() throws ConfigurationException {
     BlazeCommandRunConfigurationSettingsEditor editor =
         new BlazeCommandRunConfigurationSettingsEditor(configuration);
diff --git a/base/tests/integrationtests/com/google/idea/blaze/base/run/TestRuleHeuristicTest.java b/base/tests/integrationtests/com/google/idea/blaze/base/run/TestRuleHeuristicTest.java
index 16e7de0..aafa355 100644
--- a/base/tests/integrationtests/com/google/idea/blaze/base/run/TestRuleHeuristicTest.java
+++ b/base/tests/integrationtests/com/google/idea/blaze/base/run/TestRuleHeuristicTest.java
@@ -25,10 +25,15 @@
 import com.google.idea.blaze.base.model.primitives.Label;
 import java.io.File;
 import java.util.Collection;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
 
 /** Integration tests for {@link TestRuleHeuristic}. */
+@RunWith(JUnit4.class)
 public class TestRuleHeuristicTest extends BlazeIntegrationTestCase {
 
+  @Test
   public void testTestSizeMatched() throws Exception {
     File source = new File("java/com/foo/FooTest.java");
     Collection<RuleIdeInfo> rules =
@@ -47,6 +52,7 @@
     assertThat(match).isEqualTo(new Label("//foo:test2"));
   }
 
+  @Test
   public void testRuleNameMatched() throws Exception {
     File source = new File("java/com/foo/FooTest.java");
     Collection<RuleIdeInfo> rules =
@@ -57,6 +63,7 @@
     assertThat(match).isEqualTo(new Label("//foo:FooTest"));
   }
 
+  @Test
   public void testNoMatchFallBackToFirstRule() throws Exception {
     File source = new File("java/com/foo/FooTest.java");
     ImmutableList<RuleIdeInfo> rules =
@@ -75,6 +82,7 @@
     assertThat(match).isEqualTo(new Label("//bar:BarTest"));
   }
 
+  @Test
   public void testRuleNameCheckedBeforeTestSize() throws Exception {
     File source = new File("java/com/foo/FooTest.java");
     ImmutableList<RuleIdeInfo> rules =
diff --git a/base/tests/integrationtests/com/google/idea/blaze/base/sync/ImportRootsTest.java b/base/tests/integrationtests/com/google/idea/blaze/base/sync/ImportRootsTest.java
index 6a2dadf..139ca30 100644
--- a/base/tests/integrationtests/com/google/idea/blaze/base/sync/ImportRootsTest.java
+++ b/base/tests/integrationtests/com/google/idea/blaze/base/sync/ImportRootsTest.java
@@ -26,10 +26,15 @@
 import com.google.idea.blaze.base.settings.Blaze.BuildSystem;
 import com.google.idea.blaze.base.sync.projectview.ImportRoots;
 import java.util.stream.Collectors;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
 
 /** Tests for ImportRoots */
+@RunWith(JUnit4.class)
 public class ImportRootsTest extends BlazeIntegrationTestCase {
 
+  @Test
   public void testBazelArtifactDirectoriesExcluded() {
     ImportRoots importRoots =
         ImportRoots.builder(workspaceRoot, BuildSystem.Bazel)
@@ -52,6 +57,7 @@
     assertThat(artifactDirs).contains("bazel-" + workspaceRoot.directory().getName());
   }
 
+  @Test
   public void testNoAddedExclusionsWithoutWorkspaceRootInclusion() {
     ImportRoots importRoots =
         ImportRoots.builder(workspaceRoot, BuildSystem.Bazel)
@@ -62,6 +68,7 @@
     assertThat(importRoots.excludeDirectories()).isEmpty();
   }
 
+  @Test
   public void testNoAddedExclusionsForBlaze() {
     ImportRoots importRoots =
         ImportRoots.builder(workspaceRoot, BuildSystem.Blaze)
@@ -73,6 +80,7 @@
   }
 
   // if the workspace root is an included directory, all rules should be imported as sources.
+  @Test
   public void testAllLabelsIncludedUnderWorkspaceRoot() {
     ImportRoots importRoots =
         ImportRoots.builder(workspaceRoot, BuildSystem.Blaze)
@@ -83,6 +91,7 @@
     assertThat(importRoots.importAsSource(new Label("//foo/bar:target"))).isTrue();
   }
 
+  @Test
   public void testNonOverlappingDirectoriesAreNotFilteredOut() {
     ImportRoots importRoots =
         ImportRoots.builder(workspaceRoot, BuildSystem.Blaze)
@@ -97,6 +106,7 @@
             new WorkspacePath("root1"));
   }
 
+  @Test
   public void testOverlappingDirectoriesAreFilteredOut() {
     ImportRoots importRoots =
         ImportRoots.builder(workspaceRoot, BuildSystem.Blaze)
@@ -107,6 +117,7 @@
     assertThat(importRoots.rootDirectories()).containsExactly(new WorkspacePath("root"));
   }
 
+  @Test
   public void testWorkspaceRootIsOnlyDirectoryLeft() {
     ImportRoots importRoots =
         ImportRoots.builder(workspaceRoot, BuildSystem.Blaze)
diff --git a/base/tests/integrationtests/com/google/idea/blaze/base/lang/buildfile/lexer/AbstractLexerTest.java b/base/tests/unittests/com/google/idea/blaze/base/lang/buildfile/lexer/AbstractLexerTest.java
similarity index 98%
rename from base/tests/integrationtests/com/google/idea/blaze/base/lang/buildfile/lexer/AbstractLexerTest.java
rename to base/tests/unittests/com/google/idea/blaze/base/lang/buildfile/lexer/AbstractLexerTest.java
index d753970..12002b6 100644
--- a/base/tests/integrationtests/com/google/idea/blaze/base/lang/buildfile/lexer/AbstractLexerTest.java
+++ b/base/tests/unittests/com/google/idea/blaze/base/lang/buildfile/lexer/AbstractLexerTest.java
@@ -19,12 +19,11 @@
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertTrue;
 
+import org.junit.Ignore;
 import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.junit.runners.JUnit4;
 
 /** Tests of tokenization behavior of {@link BuildLexerBase}. */
-@RunWith(JUnit4.class)
+@Ignore
 public abstract class AbstractLexerTest {
 
   private final BuildLexerBase.LexerMode mode;
diff --git a/base/tests/integrationtests/com/google/idea/blaze/base/lang/buildfile/lexer/BlazeLexerTest.java b/base/tests/unittests/com/google/idea/blaze/base/lang/buildfile/lexer/BlazeLexerTest.java
similarity index 100%
rename from base/tests/integrationtests/com/google/idea/blaze/base/lang/buildfile/lexer/BlazeLexerTest.java
rename to base/tests/unittests/com/google/idea/blaze/base/lang/buildfile/lexer/BlazeLexerTest.java
diff --git a/base/tests/integrationtests/com/google/idea/blaze/base/lang/buildfile/lexer/HighlightingLexerTest.java b/base/tests/unittests/com/google/idea/blaze/base/lang/buildfile/lexer/HighlightingLexerTest.java
similarity index 98%
rename from base/tests/integrationtests/com/google/idea/blaze/base/lang/buildfile/lexer/HighlightingLexerTest.java
rename to base/tests/unittests/com/google/idea/blaze/base/lang/buildfile/lexer/HighlightingLexerTest.java
index 3b87a83..800d6cf 100644
--- a/base/tests/integrationtests/com/google/idea/blaze/base/lang/buildfile/lexer/HighlightingLexerTest.java
+++ b/base/tests/unittests/com/google/idea/blaze/base/lang/buildfile/lexer/HighlightingLexerTest.java
@@ -26,8 +26,7 @@
  * BuildLexerBase.LexerMode})
  */
 @RunWith(JUnit4.class)
-public class HighlightingLexerTest
-    extends com.google.idea.blaze.base.lang.buildfile.lexer.AbstractLexerTest {
+public class HighlightingLexerTest extends AbstractLexerTest {
 
   public HighlightingLexerTest() {
     super(BuildLexerBase.LexerMode.SyntaxHighlighting);
diff --git a/base/tests/unittests/com/google/idea/blaze/base/rulemaps/ReverseDependencyMapTest.java b/base/tests/unittests/com/google/idea/blaze/base/rulemaps/ReverseDependencyMapTest.java
index 310b7c3..1dbbbce 100644
--- a/base/tests/unittests/com/google/idea/blaze/base/rulemaps/ReverseDependencyMapTest.java
+++ b/base/tests/unittests/com/google/idea/blaze/base/rulemaps/ReverseDependencyMapTest.java
@@ -21,8 +21,9 @@
 import com.google.idea.blaze.base.BlazeTestCase;
 import com.google.idea.blaze.base.ideinfo.ArtifactLocation;
 import com.google.idea.blaze.base.ideinfo.RuleIdeInfo;
+import com.google.idea.blaze.base.ideinfo.RuleKey;
+import com.google.idea.blaze.base.ideinfo.RuleMap;
 import com.google.idea.blaze.base.ideinfo.RuleMapBuilder;
-import com.google.idea.blaze.base.model.RuleMap;
 import com.google.idea.blaze.base.model.primitives.Label;
 import org.jetbrains.annotations.NotNull;
 import org.junit.Test;
@@ -56,9 +57,12 @@
                     .setKind("java_library"))
             .build();
 
-    ImmutableMultimap<Label, Label> reverseDependencies =
+    ImmutableMultimap<RuleKey, RuleKey> reverseDependencies =
         ReverseDependencyMap.createRdepsMap(ruleMap);
-    assertThat(reverseDependencies).containsEntry(new Label("//l:l2"), new Label("//l:l1"));
+    assertThat(reverseDependencies)
+        .containsEntry(
+            RuleKey.forPlainTarget(new Label("//l:l2")),
+            RuleKey.forPlainTarget(new Label("//l:l1")));
   }
 
   @Test
@@ -85,10 +89,16 @@
                     .setKind("java_library"))
             .build();
 
-    ImmutableMultimap<Label, Label> reverseDependencies =
+    ImmutableMultimap<RuleKey, RuleKey> reverseDependencies =
         ReverseDependencyMap.createRdepsMap(ruleMap);
-    assertThat(reverseDependencies).containsEntry(new Label("//l:l2"), new Label("//l:l1"));
-    assertThat(reverseDependencies).containsEntry(new Label("//l:l3"), new Label("//l:l1"));
+    assertThat(reverseDependencies)
+        .containsEntry(
+            RuleKey.forPlainTarget(new Label("//l:l2")),
+            RuleKey.forPlainTarget(new Label("//l:l1")));
+    assertThat(reverseDependencies)
+        .containsEntry(
+            RuleKey.forPlainTarget(new Label("//l:l3")),
+            RuleKey.forPlainTarget(new Label("//l:l1")));
   }
 
   @Test
@@ -115,10 +125,16 @@
                     .setKind("java_library"))
             .build();
 
-    ImmutableMultimap<Label, Label> reverseDependencies =
+    ImmutableMultimap<RuleKey, RuleKey> reverseDependencies =
         ReverseDependencyMap.createRdepsMap(ruleMap);
-    assertThat(reverseDependencies).containsEntry(new Label("//l:l3"), new Label("//l:l1"));
-    assertThat(reverseDependencies).containsEntry(new Label("//l:l3"), new Label("//l:l2"));
+    assertThat(reverseDependencies)
+        .containsEntry(
+            RuleKey.forPlainTarget(new Label("//l:l3")),
+            RuleKey.forPlainTarget(new Label("//l:l1")));
+    assertThat(reverseDependencies)
+        .containsEntry(
+            RuleKey.forPlainTarget(new Label("//l:l3")),
+            RuleKey.forPlainTarget(new Label("//l:l2")));
   }
 
   @Test
@@ -157,17 +173,28 @@
                     .setKind("java_library"))
             .build();
 
-    ImmutableMultimap<Label, Label> reverseDependencies =
+    ImmutableMultimap<RuleKey, RuleKey> reverseDependencies =
         ReverseDependencyMap.createRdepsMap(ruleMap);
-    assertThat(reverseDependencies).containsEntry(new Label("//l:l3"), new Label("//l:l1"));
-    assertThat(reverseDependencies).containsEntry(new Label("//l:l3"), new Label("//l:l2"));
-    assertThat(reverseDependencies).containsEntry(new Label("//l:l3"), new Label("//l:l4"));
-    assertThat(reverseDependencies).containsEntry(new Label("//l:l4"), new Label("//l:l5"));
+    assertThat(reverseDependencies)
+        .containsEntry(
+            RuleKey.forPlainTarget(new Label("//l:l3")),
+            RuleKey.forPlainTarget(new Label("//l:l1")));
+    assertThat(reverseDependencies)
+        .containsEntry(
+            RuleKey.forPlainTarget(new Label("//l:l3")),
+            RuleKey.forPlainTarget(new Label("//l:l2")));
+    assertThat(reverseDependencies)
+        .containsEntry(
+            RuleKey.forPlainTarget(new Label("//l:l3")),
+            RuleKey.forPlainTarget(new Label("//l:l4")));
+    assertThat(reverseDependencies)
+        .containsEntry(
+            RuleKey.forPlainTarget(new Label("//l:l4")),
+            RuleKey.forPlainTarget(new Label("//l:l5")));
   }
 
   private static ArtifactLocation sourceRoot(String relativePath) {
     return ArtifactLocation.builder()
-        .setRootPath("/")
         .setRelativePath(relativePath)
         .setIsSource(true)
         .build();
diff --git a/base/tests/unittests/com/google/idea/blaze/base/run/BlazeCommandRunConfigurationTest.java b/base/tests/unittests/com/google/idea/blaze/base/run/BlazeCommandRunConfigurationTest.java
index 334557e..b8f8b22 100644
--- a/base/tests/unittests/com/google/idea/blaze/base/run/BlazeCommandRunConfigurationTest.java
+++ b/base/tests/unittests/com/google/idea/blaze/base/run/BlazeCommandRunConfigurationTest.java
@@ -58,8 +58,6 @@
         BlazeImportSettingsManager.class, new BlazeImportSettingsManager(project));
     BlazeImportSettingsManager.getInstance(getProject()).setImportSettings(DUMMY_IMPORT_SETTINGS);
 
-    this.configuration = this.type.getFactory().createTemplateConfiguration(project);
-
     applicationServices.register(ExperimentService.class, new MockExperimentService());
     applicationServices.register(RuleFinder.class, new MockRuleFinder());
     ExtensionPointImpl<BlazeCommandRunConfigurationHandlerProvider> handlerProviderEp =
@@ -67,6 +65,8 @@
             BlazeCommandRunConfigurationHandlerProvider.EP_NAME,
             BlazeCommandRunConfigurationHandlerProvider.class);
     handlerProviderEp.registerExtension(new MockBlazeCommandRunConfigurationHandlerProvider());
+
+    this.configuration = this.type.getFactory().createTemplateConfiguration(project);
   }
 
   @Test
diff --git a/base/tests/unittests/com/google/idea/blaze/base/run/RuleNameHeuristicTest.java b/base/tests/unittests/com/google/idea/blaze/base/run/RuleNameHeuristicTest.java
index 1cb0276..1dbec9d 100644
--- a/base/tests/unittests/com/google/idea/blaze/base/run/RuleNameHeuristicTest.java
+++ b/base/tests/unittests/com/google/idea/blaze/base/run/RuleNameHeuristicTest.java
@@ -49,6 +49,29 @@
   }
 
   @Test
+  public void testPredicateMatchingNameAndPath() throws Exception {
+    File source = new File("java/com/foo/FooTest.java");
+    RuleIdeInfo rule =
+        RuleIdeInfo.builder().setLabel("//foo:foo/FooTest").setKind("java_test").build();
+    assertThat(new RuleNameHeuristic().matchesSource(rule, source, null)).isTrue();
+  }
+
+  @Test
+  public void testPredicateNotMatchingForPartialOverlap() throws Exception {
+    File source = new File("java/com/foo/BarFooTest.java");
+    RuleIdeInfo rule = RuleIdeInfo.builder().setLabel("//foo:FooTest").setKind("java_test").build();
+    assertThat(new RuleNameHeuristic().matchesSource(rule, source, null)).isFalse();
+  }
+
+  @Test
+  public void testPredicateNotMatchingIncorrectPath() throws Exception {
+    File source = new File("java/com/foo/FooTest.java");
+    RuleIdeInfo rule =
+        RuleIdeInfo.builder().setLabel("//foo:bar/FooTest").setKind("java_test").build();
+    assertThat(new RuleNameHeuristic().matchesSource(rule, source, null)).isFalse();
+  }
+
+  @Test
   public void testPredicateDifferentName() throws Exception {
     File source = new File("java/com/foo/FooTest.java");
     RuleIdeInfo rule = RuleIdeInfo.builder().setLabel("//foo:ForTest").setKind("java_test").build();
diff --git a/base/tests/unittests/com/google/idea/blaze/base/run/confighandler/BlazeCommandGenericRunConfigurationHandlerTest.java b/base/tests/unittests/com/google/idea/blaze/base/run/confighandler/BlazeCommandGenericRunConfigurationHandlerTest.java
deleted file mode 100644
index 64d3900..0000000
--- a/base/tests/unittests/com/google/idea/blaze/base/run/confighandler/BlazeCommandGenericRunConfigurationHandlerTest.java
+++ /dev/null
@@ -1,158 +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.run.confighandler;
-
-import static com.google.common.truth.Truth.assertThat;
-
-import com.google.common.collect.ImmutableList;
-import com.google.common.collect.Lists;
-import com.google.idea.blaze.base.BlazeTestCase;
-import com.google.idea.blaze.base.command.BlazeCommandName;
-import com.google.idea.blaze.base.run.BlazeCommandRunConfiguration;
-import com.google.idea.blaze.base.run.BlazeCommandRunConfigurationType;
-import com.google.idea.blaze.base.run.confighandler.BlazeCommandGenericRunConfigurationHandler.BlazeCommandGenericRunConfigurationHandlerEditor;
-import com.google.idea.blaze.base.settings.Blaze;
-import com.google.idea.blaze.base.settings.BlazeImportSettings;
-import com.google.idea.blaze.base.settings.BlazeImportSettingsManager;
-import com.intellij.ide.ui.UISettings;
-import com.intellij.openapi.options.ConfigurationException;
-import com.intellij.openapi.util.InvalidDataException;
-import org.jdom.Element;
-import org.jetbrains.annotations.NotNull;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.junit.runners.JUnit4;
-
-/** Tests for {@link BlazeCommandGenericRunConfigurationHandler}. */
-@RunWith(JUnit4.class)
-public class BlazeCommandGenericRunConfigurationHandlerTest extends BlazeTestCase {
-  private static final BlazeImportSettings DUMMY_IMPORT_SETTINGS =
-      new BlazeImportSettings("", "", "", "", "", Blaze.BuildSystem.Blaze);
-  private static final BlazeCommandName COMMAND = BlazeCommandName.fromString("command");
-
-  private final BlazeCommandRunConfigurationType type = new BlazeCommandRunConfigurationType();
-  private BlazeCommandRunConfiguration configuration;
-  private BlazeCommandGenericRunConfigurationHandler handler;
-
-  @Override
-  protected void initTest(
-      @NotNull Container applicationServices, @NotNull Container projectServices) {
-    super.initTest(applicationServices, projectServices);
-
-    applicationServices.register(UISettings.class, new UISettings());
-    projectServices.register(
-        BlazeImportSettingsManager.class, new BlazeImportSettingsManager(project));
-    BlazeImportSettingsManager.getInstance(getProject()).setImportSettings(DUMMY_IMPORT_SETTINGS);
-
-    configuration = type.getFactory().createTemplateConfiguration(project);
-    handler = new BlazeCommandGenericRunConfigurationHandler(configuration);
-  }
-
-  @Test
-  public void readAndWriteShouldMatch() throws InvalidDataException {
-    handler.setCommand(COMMAND);
-    handler.setBlazeFlags(ImmutableList.of("--flag1", "--flag2"));
-    handler.setExeFlags(ImmutableList.of("--exeFlag1"));
-    handler.setBlazeBinary("/usr/bin/blaze");
-
-    Element element = new Element("test");
-    handler.writeExternal(element);
-    BlazeCommandRunConfiguration readConfiguration =
-        type.getFactory().createTemplateConfiguration(project);
-    BlazeCommandGenericRunConfigurationHandler readHandler =
-        new BlazeCommandGenericRunConfigurationHandler(readConfiguration);
-    readHandler.readExternal(element);
-
-    assertThat(readHandler.getCommand()).isEqualTo(COMMAND);
-    assertThat(readHandler.getAllBlazeFlags()).containsExactly("--flag1", "--flag2").inOrder();
-    assertThat(readHandler.getAllExeFlags()).containsExactly("--exeFlag1");
-    assertThat(readHandler.getBlazeBinary()).isEqualTo("/usr/bin/blaze");
-  }
-
-  @Test
-  public void readAndWriteShouldHandleNulls() throws InvalidDataException {
-    Element element = new Element("test");
-    handler.writeExternal(element);
-    BlazeCommandRunConfiguration readConfiguration =
-        type.getFactory().createTemplateConfiguration(project);
-    BlazeCommandGenericRunConfigurationHandler readHandler =
-        new BlazeCommandGenericRunConfigurationHandler(readConfiguration);
-    readHandler.readExternal(element);
-
-    assertThat(readHandler.getCommand()).isEqualTo(handler.getCommand());
-    assertThat(readHandler.getAllBlazeFlags()).isEqualTo(handler.getAllBlazeFlags());
-    assertThat(readHandler.getAllExeFlags()).isEqualTo(handler.getAllExeFlags());
-    assertThat(readHandler.getBlazeBinary()).isEqualTo(handler.getBlazeBinary());
-  }
-
-  @Test
-  public void readShouldOmitEmptyFlags() throws InvalidDataException {
-    handler.setBlazeFlags(Lists.newArrayList("hi ", "", "I'm", " ", "\t", "Josh\r\n", "\n"));
-    handler.setExeFlags(Lists.newArrayList("hi ", "", "I'm", " ", "\t", "Josh\r\n", "\n"));
-
-    Element element = new Element("test");
-    handler.writeExternal(element);
-    BlazeCommandRunConfiguration readConfiguration =
-        type.getFactory().createTemplateConfiguration(project);
-    BlazeCommandGenericRunConfigurationHandler readHandler =
-        new BlazeCommandGenericRunConfigurationHandler(readConfiguration);
-    readHandler.readExternal(element);
-
-    assertThat(readHandler.getAllBlazeFlags()).containsExactly("hi", "I'm", "Josh").inOrder();
-    assertThat(readHandler.getAllExeFlags()).containsExactly("hi", "I'm", "Josh").inOrder();
-  }
-
-  @Test
-  public void editorApplyToAndResetFromShouldMatch() throws ConfigurationException {
-    BlazeCommandGenericRunConfigurationHandlerEditor editor =
-        new BlazeCommandGenericRunConfigurationHandlerEditor(handler);
-
-    handler.setCommand(COMMAND);
-    handler.setBlazeFlags(ImmutableList.of("--flag1", "--flag2"));
-    handler.setExeFlags(ImmutableList.of("--exeFlag1", "--exeFlag2"));
-    handler.setBlazeBinary("/usr/bin/blaze");
-
-    editor.resetEditorFrom(handler);
-    BlazeCommandRunConfiguration readConfiguration =
-        type.getFactory().createTemplateConfiguration(project);
-    BlazeCommandGenericRunConfigurationHandler readHandler =
-        new BlazeCommandGenericRunConfigurationHandler(readConfiguration);
-    editor.applyEditorTo(readHandler);
-
-    assertThat(readHandler.getCommand()).isEqualTo(handler.getCommand());
-    assertThat(readHandler.getAllBlazeFlags()).isEqualTo(handler.getAllBlazeFlags());
-    assertThat(readHandler.getAllExeFlags()).isEqualTo(handler.getAllExeFlags());
-    assertThat(readHandler.getBlazeBinary()).isEqualTo(handler.getBlazeBinary());
-  }
-
-  @Test
-  public void editorApplyToAndResetFromShouldHandleNulls() throws ConfigurationException {
-    BlazeCommandGenericRunConfigurationHandlerEditor editor =
-        new BlazeCommandGenericRunConfigurationHandlerEditor(handler);
-
-    editor.resetEditorFrom(handler);
-    BlazeCommandRunConfiguration readConfiguration =
-        type.getFactory().createTemplateConfiguration(project);
-    BlazeCommandGenericRunConfigurationHandler readHandler =
-        new BlazeCommandGenericRunConfigurationHandler(readConfiguration);
-    editor.applyEditorTo(readHandler);
-
-    assertThat(readHandler.getCommand()).isEqualTo(handler.getCommand());
-    assertThat(readHandler.getAllBlazeFlags()).isEqualTo(handler.getAllBlazeFlags());
-    assertThat(readHandler.getAllExeFlags()).isEqualTo(handler.getAllExeFlags());
-    assertThat(readHandler.getBlazeBinary()).isEqualTo(handler.getBlazeBinary());
-  }
-}
diff --git a/base/tests/unittests/com/google/idea/blaze/base/run/confighandler/BlazeUnknownRunConfigurationHandlerTest.java b/base/tests/unittests/com/google/idea/blaze/base/run/confighandler/BlazeUnknownRunConfigurationHandlerTest.java
deleted file mode 100644
index 7752f26..0000000
--- a/base/tests/unittests/com/google/idea/blaze/base/run/confighandler/BlazeUnknownRunConfigurationHandlerTest.java
+++ /dev/null
@@ -1,159 +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.run.confighandler;
-
-import static com.google.common.truth.Truth.assertThat;
-
-import com.google.idea.blaze.base.BlazeTestCase;
-import com.google.idea.blaze.base.run.BlazeCommandRunConfiguration;
-import com.google.idea.blaze.base.run.BlazeCommandRunConfigurationType;
-import com.google.idea.blaze.base.settings.Blaze;
-import com.google.idea.blaze.base.settings.BlazeImportSettings;
-import com.google.idea.blaze.base.settings.BlazeImportSettingsManager;
-import com.intellij.ide.ui.UISettings;
-import com.intellij.openapi.util.InvalidDataException;
-import java.io.StringReader;
-import org.jdom.Element;
-import org.jdom.input.SAXBuilder;
-import org.jdom.output.Format;
-import org.jdom.output.XMLOutputter;
-import org.jetbrains.annotations.NotNull;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.junit.runners.JUnit4;
-
-/** Tests for {@link BlazeUnknownRunConfigurationHandler}. */
-@RunWith(JUnit4.class)
-public class BlazeUnknownRunConfigurationHandlerTest extends BlazeTestCase {
-  private static final BlazeImportSettings DUMMY_IMPORT_SETTINGS =
-      new BlazeImportSettings("", "", "", "", "", Blaze.BuildSystem.Blaze);
-
-  private final BlazeCommandRunConfigurationType type = new BlazeCommandRunConfigurationType();
-  private BlazeCommandRunConfiguration configuration;
-  private BlazeUnknownRunConfigurationHandler handler;
-
-  @Override
-  protected void initTest(
-      @NotNull Container applicationServices, @NotNull Container projectServices) {
-    super.initTest(applicationServices, projectServices);
-
-    applicationServices.register(UISettings.class, new UISettings());
-    projectServices.register(
-        BlazeImportSettingsManager.class, new BlazeImportSettingsManager(project));
-    BlazeImportSettingsManager.getInstance(getProject()).setImportSettings(DUMMY_IMPORT_SETTINGS);
-
-    configuration = type.getFactory().createTemplateConfiguration(project);
-    handler = new BlazeUnknownRunConfigurationHandler(configuration);
-  }
-
-  @Test
-  public void readAndWriteShouldPreserveOldContent() throws Exception {
-    SAXBuilder saxBuilder = new SAXBuilder();
-    XMLOutputter xmlOutputter = new XMLOutputter(Format.getCompactFormat());
-
-    String inputXml =
-        "<?xml version=\"1.0\"?>"
-            + "<test foo=\"bar\" bar=\"baz\">"
-            + "  <child abc=\"def\">"
-            + "    <grandchild />"
-            + "  </child>"
-            + "  <child foo=\"baz\" />"
-            + "</test>";
-    Element element = saxBuilder.build(new StringReader(inputXml)).getRootElement();
-    handler.readExternal(element);
-
-    Element writeElement = new Element("test");
-    handler.writeExternal(writeElement);
-
-    assertThat(xmlOutputter.outputString(writeElement))
-        .isEqualTo(xmlOutputter.outputString(element));
-  }
-
-  @Test
-  public void readAndWriteShouldHandleEmptyElements() throws InvalidDataException {
-    //<test />
-    Element element = new Element("test");
-    handler.readExternal(element);
-
-    Element writeElement = new Element("test");
-    handler.writeExternal(writeElement);
-
-    assertThat(writeElement.getAttributes()).isEmpty();
-    assertThat(writeElement.getChildren()).isEmpty();
-  }
-
-  @Test
-  public void writeShouldPreserveNewContent() throws Exception {
-    SAXBuilder saxBuilder = new SAXBuilder();
-    XMLOutputter xmlOutputter = new XMLOutputter(Format.getCompactFormat());
-
-    //<test />
-    Element element = new Element("test");
-    handler.readExternal(element);
-
-    String newXml =
-        "<?xml version=\"1.0\"?>"
-            + "<test foo=\"bar\">"
-            + "  <child abc=\"def\" />"
-            + "  <child />"
-            + "</test>";
-    Element writeElement = saxBuilder.build(new StringReader(newXml)).getRootElement();
-    handler.writeExternal(writeElement);
-
-    Element newElement = saxBuilder.build(new StringReader(newXml)).getRootElement();
-    assertThat(xmlOutputter.outputString(writeElement))
-        .isEqualTo(xmlOutputter.outputString(newElement));
-  }
-
-  @Test
-  public void writeShouldMergeAndOverwriteOldContent() throws Exception {
-    SAXBuilder saxBuilder = new SAXBuilder();
-    XMLOutputter xmlOutputter = new XMLOutputter(Format.getCompactFormat());
-
-    String oldXml =
-        "<?xml version=\"1.0\"?>"
-            + "<test foo=\"old\" bar=\"old\">"
-            + "  <child abc=\"old\">"
-            + "    <grandchild />"
-            + "  </child>"
-            + "  <backup foo=\"baz\" />"
-            + "  <backup />"
-            + "</test>";
-    Element element = saxBuilder.build(new StringReader(oldXml)).getRootElement();
-    handler.readExternal(element);
-
-    String newXml =
-        "<?xml version=\"1.0\"?>"
-            + "<test foo=\"bar\">"
-            + "  <child abc=\"def\" />"
-            + "  <child />"
-            + "</test>";
-    Element writeElement = saxBuilder.build(new StringReader(newXml)).getRootElement();
-    handler.writeExternal(writeElement);
-
-    String mergedXml =
-        "<?xml version=\"1.0\"?>"
-            + "<test foo=\"bar\" bar=\"old\">"
-            + "  <child abc=\"def\" />"
-            + "  <child />"
-            + "  <backup foo=\"baz\" />"
-            + "  <backup />"
-            + "</test>";
-    Element mergedElement = saxBuilder.build(new StringReader(mergedXml)).getRootElement();
-    assertThat(xmlOutputter.outputString(writeElement))
-        .isEqualTo(xmlOutputter.outputString(mergedElement));
-  }
-}
diff --git a/base/tests/unittests/com/google/idea/blaze/base/run/state/BlazeCommandRunConfigurationCommonStateTest.java b/base/tests/unittests/com/google/idea/blaze/base/run/state/BlazeCommandRunConfigurationCommonStateTest.java
new file mode 100644
index 0000000..307722c
--- /dev/null
+++ b/base/tests/unittests/com/google/idea/blaze/base/run/state/BlazeCommandRunConfigurationCommonStateTest.java
@@ -0,0 +1,138 @@
+/*
+ * Copyright 2016 The Bazel Authors. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.idea.blaze.base.run.state;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Lists;
+import com.google.idea.blaze.base.BlazeTestCase;
+import com.google.idea.blaze.base.command.BlazeCommandName;
+import com.google.idea.blaze.base.settings.Blaze;
+import com.google.idea.blaze.base.settings.BlazeImportSettings;
+import com.google.idea.blaze.base.settings.BlazeImportSettingsManager;
+import com.intellij.ide.ui.UISettings;
+import org.jdom.Element;
+import org.jetbrains.annotations.NotNull;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** Tests for {@link BlazeCommandRunConfigurationCommonState}. */
+@RunWith(JUnit4.class)
+public class BlazeCommandRunConfigurationCommonStateTest extends BlazeTestCase {
+  private static final BlazeImportSettings DUMMY_IMPORT_SETTINGS =
+      new BlazeImportSettings("", "", "", "", "", Blaze.BuildSystem.Blaze);
+  private static final BlazeCommandName COMMAND = BlazeCommandName.fromString("command");
+
+  private BlazeCommandRunConfigurationCommonState state;
+
+  @Override
+  protected void initTest(
+      @NotNull Container applicationServices, @NotNull Container projectServices) {
+    super.initTest(applicationServices, projectServices);
+
+    applicationServices.register(UISettings.class, new UISettings());
+    projectServices.register(
+        BlazeImportSettingsManager.class, new BlazeImportSettingsManager(project));
+    BlazeImportSettingsManager.getInstance(getProject()).setImportSettings(DUMMY_IMPORT_SETTINGS);
+
+    state = new BlazeCommandRunConfigurationCommonState(Blaze.buildSystemName(project));
+  }
+
+  @Test
+  public void readAndWriteShouldMatch() throws Exception {
+    state.setCommand(COMMAND);
+    state.setBlazeFlags(ImmutableList.of("--flag1", "--flag2"));
+    state.setExeFlags(ImmutableList.of("--exeFlag1"));
+    state.setBlazeBinary("/usr/bin/blaze");
+
+    Element element = new Element("test");
+    state.writeExternal(element);
+    BlazeCommandRunConfigurationCommonState readState =
+        new BlazeCommandRunConfigurationCommonState(Blaze.buildSystemName(project));
+    readState.readExternal(element);
+
+    assertThat(readState.getCommand()).isEqualTo(COMMAND);
+    assertThat(readState.getBlazeFlags()).containsExactly("--flag1", "--flag2").inOrder();
+    assertThat(readState.getExeFlags()).containsExactly("--exeFlag1");
+    assertThat(readState.getBlazeBinary()).isEqualTo("/usr/bin/blaze");
+  }
+
+  @Test
+  public void readAndWriteShouldHandleNulls() throws Exception {
+    Element element = new Element("test");
+    state.writeExternal(element);
+    BlazeCommandRunConfigurationCommonState readState =
+        new BlazeCommandRunConfigurationCommonState(Blaze.buildSystemName(project));
+    readState.readExternal(element);
+
+    assertThat(readState.getCommand()).isEqualTo(state.getCommand());
+    assertThat(readState.getBlazeFlags()).isEqualTo(state.getBlazeFlags());
+    assertThat(readState.getExeFlags()).isEqualTo(state.getExeFlags());
+    assertThat(readState.getBlazeBinary()).isEqualTo(state.getBlazeBinary());
+  }
+
+  @Test
+  public void readShouldOmitEmptyFlags() throws Exception {
+    state.setBlazeFlags(Lists.newArrayList("hi ", "", "I'm", " ", "\t", "Josh\r\n", "\n"));
+    state.setExeFlags(Lists.newArrayList("hi ", "", "I'm", " ", "\t", "Josh\r\n", "\n"));
+
+    Element element = new Element("test");
+    state.writeExternal(element);
+    BlazeCommandRunConfigurationCommonState readState =
+        new BlazeCommandRunConfigurationCommonState(Blaze.buildSystemName(project));
+    readState.readExternal(element);
+
+    assertThat(readState.getBlazeFlags()).containsExactly("hi", "I'm", "Josh").inOrder();
+    assertThat(readState.getExeFlags()).containsExactly("hi", "I'm", "Josh").inOrder();
+  }
+
+  @Test
+  public void editorApplyToAndResetFromShouldMatch() throws Exception {
+    RunConfigurationStateEditor editor = state.getEditor(project);
+
+    state.setCommand(COMMAND);
+    state.setBlazeFlags(ImmutableList.of("--flag1", "--flag2"));
+    state.setExeFlags(ImmutableList.of("--exeFlag1", "--exeFlag2"));
+    state.setBlazeBinary("/usr/bin/blaze");
+
+    editor.resetEditorFrom(state);
+    BlazeCommandRunConfigurationCommonState readState =
+        new BlazeCommandRunConfigurationCommonState(Blaze.buildSystemName(project));
+    editor.applyEditorTo(readState);
+
+    assertThat(readState.getCommand()).isEqualTo(state.getCommand());
+    assertThat(readState.getBlazeFlags()).isEqualTo(state.getBlazeFlags());
+    assertThat(readState.getExeFlags()).isEqualTo(state.getExeFlags());
+    assertThat(readState.getBlazeBinary()).isEqualTo(state.getBlazeBinary());
+  }
+
+  @Test
+  public void editorApplyToAndResetFromShouldHandleNulls() throws Exception {
+    RunConfigurationStateEditor editor = state.getEditor(project);
+
+    editor.resetEditorFrom(state);
+    BlazeCommandRunConfigurationCommonState readState =
+        new BlazeCommandRunConfigurationCommonState(Blaze.buildSystemName(project));
+    editor.applyEditorTo(readState);
+
+    assertThat(readState.getCommand()).isEqualTo(state.getCommand());
+    assertThat(readState.getBlazeFlags()).isEqualTo(state.getBlazeFlags());
+    assertThat(readState.getExeFlags()).isEqualTo(state.getExeFlags());
+    assertThat(readState.getBlazeBinary()).isEqualTo(state.getBlazeBinary());
+  }
+}
diff --git a/base/tests/unittests/com/google/idea/blaze/base/run/testmap/TestMapTest.java b/base/tests/unittests/com/google/idea/blaze/base/run/testmap/TestMapTest.java
index acb1a01..b3dfb93 100644
--- a/base/tests/unittests/com/google/idea/blaze/base/run/testmap/TestMapTest.java
+++ b/base/tests/unittests/com/google/idea/blaze/base/run/testmap/TestMapTest.java
@@ -21,11 +21,13 @@
 import com.google.idea.blaze.base.BlazeTestCase;
 import com.google.idea.blaze.base.ideinfo.ArtifactLocation;
 import com.google.idea.blaze.base.ideinfo.RuleIdeInfo;
+import com.google.idea.blaze.base.ideinfo.RuleKey;
+import com.google.idea.blaze.base.ideinfo.RuleMap;
 import com.google.idea.blaze.base.ideinfo.RuleMapBuilder;
-import com.google.idea.blaze.base.model.RuleMap;
 import com.google.idea.blaze.base.model.primitives.Label;
 import com.google.idea.blaze.base.rulemaps.ReverseDependencyMap;
 import com.google.idea.blaze.base.run.testmap.TestRuleFinderImpl.TestMap;
+import com.google.idea.blaze.base.sync.workspace.ArtifactLocationDecoder;
 import com.google.idea.common.experiments.ExperimentService;
 import com.google.idea.common.experiments.MockExperimentService;
 import java.io.File;
@@ -39,6 +41,10 @@
 public class TestMapTest extends BlazeTestCase {
   private RuleMapBuilder ruleMapBuilder;
 
+  private final ArtifactLocationDecoder artifactLocationDecoder =
+      (ArtifactLocationDecoder)
+          artifactLocation -> new File("/", artifactLocation.getRelativePath());
+
   @Override
   protected void initTest(
       @NotNull Container applicationServices, @NotNull Container projectServices) {
@@ -59,8 +65,8 @@
                     .addSource(sourceRoot("test/Test.java")))
             .build();
 
-    TestMap testMap = new TestMap(project, ruleMap);
-    ImmutableMultimap<Label, Label> reverseDependencies =
+    TestMap testMap = new TestMap(project, artifactLocationDecoder, ruleMap);
+    ImmutableMultimap<RuleKey, RuleKey> reverseDependencies =
         ReverseDependencyMap.createRdepsMap(ruleMap);
     assertThat(testMap.testTargetsForSourceFile(reverseDependencies, new File("/test/Test.java")))
         .containsExactly(new Label("//test:test"));
@@ -84,8 +90,8 @@
                     .addSource(sourceRoot("test/Test.java")))
             .build();
 
-    TestMap testMap = new TestMap(project, ruleMap);
-    ImmutableMultimap<Label, Label> reverseDependencies =
+    TestMap testMap = new TestMap(project, artifactLocationDecoder, ruleMap);
+    ImmutableMultimap<RuleKey, RuleKey> reverseDependencies =
         ReverseDependencyMap.createRdepsMap(ruleMap);
     assertThat(testMap.testTargetsForSourceFile(reverseDependencies, new File("/test/Test.java")))
         .containsExactly(new Label("//test:test"));
@@ -115,8 +121,8 @@
                     .addSource(sourceRoot("test/Test.java")))
             .build();
 
-    TestMap testMap = new TestMap(project, ruleMap);
-    ImmutableMultimap<Label, Label> reverseDependencies =
+    TestMap testMap = new TestMap(project, artifactLocationDecoder, ruleMap);
+    ImmutableMultimap<RuleKey, RuleKey> reverseDependencies =
         ReverseDependencyMap.createRdepsMap(ruleMap);
     assertThat(testMap.testTargetsForSourceFile(reverseDependencies, new File("/test/Test.java")))
         .containsExactly(new Label("//test:test"), new Label("//test:test2"));
@@ -152,8 +158,8 @@
                     .addDependency("//test:lib"))
             .build();
 
-    TestMap testMap = new TestMap(project, ruleMap);
-    ImmutableMultimap<Label, Label> reverseDependencies =
+    TestMap testMap = new TestMap(project, artifactLocationDecoder, ruleMap);
+    ImmutableMultimap<RuleKey, RuleKey> reverseDependencies =
         ReverseDependencyMap.createRdepsMap(ruleMap);
     assertThat(testMap.testTargetsForSourceFile(reverseDependencies, new File("/test/Test.java")))
         .containsExactly(new Label("//test:test"), new Label("//test:test2"))
@@ -190,8 +196,8 @@
                     .addSource(sourceRoot("test/Test.java")))
             .build();
 
-    TestMap testMap = new TestMap(project, ruleMap);
-    ImmutableMultimap<Label, Label> reverseDependencies =
+    TestMap testMap = new TestMap(project, artifactLocationDecoder, ruleMap);
+    ImmutableMultimap<RuleKey, RuleKey> reverseDependencies =
         ReverseDependencyMap.createRdepsMap(ruleMap);
     assertThat(testMap.testTargetsForSourceFile(reverseDependencies, new File("/test/Test.java")))
         .containsExactly(new Label("//test:test"), new Label("//test:test2"));
@@ -222,8 +228,8 @@
                     .addSource(sourceRoot("test/Test.java")))
             .build();
 
-    TestMap testMap = new TestMap(project, ruleMap);
-    ImmutableMultimap<Label, Label> reverseDependencies =
+    TestMap testMap = new TestMap(project, artifactLocationDecoder, ruleMap);
+    ImmutableMultimap<RuleKey, RuleKey> reverseDependencies =
         ReverseDependencyMap.createRdepsMap(ruleMap);
     assertThat(testMap.testTargetsForSourceFile(reverseDependencies, new File("/test/Test.java")))
         .containsExactly(new Label("//test:test"));
@@ -231,7 +237,6 @@
 
   private ArtifactLocation sourceRoot(String relativePath) {
     return ArtifactLocation.builder()
-        .setRootPath("/")
         .setRelativePath(relativePath)
         .setIsSource(true)
         .build();
diff --git a/base/tests/unittests/com/google/idea/blaze/base/sync/aspects/BlazeIdeInterfaceAspectsImplTest.java b/base/tests/unittests/com/google/idea/blaze/base/sync/aspects/BlazeIdeInterfaceAspectsImplTest.java
index 24a6200..7d005db 100644
--- a/base/tests/unittests/com/google/idea/blaze/base/sync/aspects/BlazeIdeInterfaceAspectsImplTest.java
+++ b/base/tests/unittests/com/google/idea/blaze/base/sync/aspects/BlazeIdeInterfaceAspectsImplTest.java
@@ -15,23 +15,17 @@
  */
 package com.google.idea.blaze.base.sync.aspects;
 
-import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableMap;
 import com.google.common.collect.ImmutableSet;
 import com.google.idea.blaze.base.BlazeTestCase;
 import com.google.idea.blaze.base.TestUtils;
 import com.google.idea.blaze.base.ideinfo.RuleIdeInfo;
+import com.google.idea.blaze.base.ideinfo.RuleMap;
 import com.google.idea.blaze.base.io.FileAttributeProvider;
-import com.google.idea.blaze.base.model.RuleMap;
-import com.google.idea.blaze.base.model.primitives.ExecutionRootPath;
 import com.google.idea.blaze.base.model.primitives.Label;
 import com.google.idea.blaze.base.model.primitives.LanguageClass;
-import com.google.idea.blaze.base.model.primitives.WorkspaceRoot;
 import com.google.idea.blaze.base.model.primitives.WorkspaceType;
 import com.google.idea.blaze.base.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.WorkspacePathResolverImpl;
 import com.google.idea.common.experiments.ExperimentService;
 import com.google.idea.common.experiments.MockExperimentService;
 import com.google.repackaged.devtools.build.lib.ideinfo.androidstudio.AndroidStudioIdeInfo;
@@ -46,16 +40,6 @@
 public class BlazeIdeInterfaceAspectsImplTest extends BlazeTestCase {
 
   private static final File DUMMY_ROOT = new File("/");
-  private static final WorkspaceRoot WORKSPACE_ROOT = new WorkspaceRoot(DUMMY_ROOT);
-  private static final BlazeRoots BLAZE_ROOTS =
-      new BlazeRoots(
-          DUMMY_ROOT,
-          ImmutableList.of(DUMMY_ROOT),
-          new ExecutionRootPath("out/crosstool/bin"),
-          new ExecutionRootPath("out/crosstool/gen"));
-  private static final ArtifactLocationDecoder DUMMY_DECODER =
-      new ArtifactLocationDecoder(
-          BLAZE_ROOTS, new WorkspacePathResolverImpl(WORKSPACE_ROOT, BLAZE_ROOTS));
 
   @Override
   protected void initTest(
@@ -96,14 +80,17 @@
         new WorkspaceLanguageSettings(
             WorkspaceType.ANDROID, ImmutableSet.of(LanguageClass.ANDROID));
     RuleIdeInfo ruleIdeInfo =
-        IdeInfoFromProtobuf.makeRuleIdeInfo(workspaceLanguageSettings, DUMMY_DECODER, ideProto);
+        IdeInfoFromProtobuf.makeRuleIdeInfo(workspaceLanguageSettings, ideProto);
     TestUtils.assertIsSerializable(ruleIdeInfo);
   }
 
   @Test
   public void testBlazeStateIsSerializable() {
     BlazeIdeInterfaceAspectsImpl.State state = new BlazeIdeInterfaceAspectsImpl.State();
-    state.fileToLabel = ImmutableMap.of(new File("fileName"), new Label("//java/com/test:test"));
+    state.fileToRuleMapKey =
+        ImmutableMap.of(
+            new File("fileName"),
+            RuleIdeInfo.builder().setLabel(new Label("//test:test")).build().key);
     state.fileState = ImmutableMap.of();
     state.ruleMap =
         new RuleMap(ImmutableMap.of()); // Tested separately in testRuleIdeInfoIsSerializable
@@ -117,8 +104,6 @@
 
   static AndroidStudioIdeInfo.ArtifactLocation artifactLocation(
       String rootPath, String relativePath) {
-    return AndroidStudioIdeInfo.ArtifactLocation.newBuilder()
-        .setRelativePath(relativePath)
-        .build();
+    return AndroidStudioIdeInfo.ArtifactLocation.newBuilder().setRelativePath(relativePath).build();
   }
 }
diff --git a/base/tests/unittests/com/google/idea/blaze/base/sync/workspace/ArtifactLocationDecoderTest.java b/base/tests/unittests/com/google/idea/blaze/base/sync/workspace/ArtifactLocationDecoderTest.java
index c6ea540..02849cd 100644
--- a/base/tests/unittests/com/google/idea/blaze/base/sync/workspace/ArtifactLocationDecoderTest.java
+++ b/base/tests/unittests/com/google/idea/blaze/base/sync/workspace/ArtifactLocationDecoderTest.java
@@ -25,8 +25,6 @@
 import com.google.idea.blaze.base.io.FileAttributeProvider;
 import com.google.idea.blaze.base.model.primitives.ExecutionRootPath;
 import com.google.idea.blaze.base.model.primitives.WorkspaceRoot;
-import com.google.repackaged.devtools.build.lib.ideinfo.androidstudio.AndroidStudioIdeInfo;
-import com.google.repackaged.devtools.build.lib.ideinfo.androidstudio.PackageManifestOuterClass;
 import java.io.File;
 import java.util.List;
 import java.util.Set;
@@ -39,25 +37,8 @@
 @RunWith(JUnit4.class)
 public class ArtifactLocationDecoderTest extends BlazeTestCase {
 
-  private static final WorkspaceRoot WORKSPACE_ROOT = new WorkspaceRoot(new File("/path/to/root"));
   private static final String EXECUTION_ROOT = "/path/to/_blaze_user/1234bf129e/root";
 
-  private static final BlazeRoots BLAZE_GIT5_ROOTS =
-      new BlazeRoots(
-          new File(EXECUTION_ROOT),
-          ImmutableList.of(
-              WORKSPACE_ROOT.directory(),
-              new File(WORKSPACE_ROOT.directory().getParentFile(), "READONLY/root")),
-          new ExecutionRootPath("root/blaze-out/crosstool/bin"),
-          new ExecutionRootPath("root/blaze-out/crosstool/genfiles"));
-
-  private static final BlazeRoots BLAZE_CITC_ROOTS =
-      new BlazeRoots(
-          new File(EXECUTION_ROOT),
-          ImmutableList.of(WORKSPACE_ROOT.directory()),
-          new ExecutionRootPath("root/blaze-out/crosstool/bin"),
-          new ExecutionRootPath("root/blaze-out/crosstool/genfiles"));
-
   static class MockFileAttributeProvider extends FileAttributeProvider {
     final Set<File> files = Sets.newHashSet();
 
@@ -86,9 +67,9 @@
   public void testManualPackagePaths() throws Exception {
     List<File> packagePaths =
         ImmutableList.of(
-            WORKSPACE_ROOT.directory(),
-            new File(WORKSPACE_ROOT.directory().getParentFile(), "READONLY/root"),
-            new File(WORKSPACE_ROOT.directory().getParentFile(), "CUSTOM/root"));
+            new File("/path/to"),
+            new File("/path/to/READONLY/root"),
+            new File("/path/to/CUSTOM/root"));
 
     BlazeRoots blazeRoots =
         new BlazeRoots(
@@ -98,117 +79,55 @@
             new ExecutionRootPath("root/blaze-out/crosstool/genfiles"));
 
     fileChecker.addFiles(
-        new File(packagePaths.get(0), "com/google/Bla.java"),
-        new File(packagePaths.get(1), "com/google/Foo.java"),
-        new File(packagePaths.get(2), "com/other/Test.java"));
+        new File("/path/to/com/google/Bla.java"),
+        new File("/path/to/READONLY/root/com/google/Foo.java"),
+        new File("/path/to/CUSTOM/root/com/other/Test.java"));
 
     ArtifactLocationDecoder decoder =
-        new ArtifactLocationDecoder(
-            blazeRoots, new WorkspacePathResolverImpl(WORKSPACE_ROOT, blazeRoots));
+        new ArtifactLocationDecoderImpl(
+            blazeRoots,
+            new WorkspacePathResolverImpl(
+                new WorkspaceRoot(new File("/path/to/root")), blazeRoots));
 
-    ArtifactLocationBuilder builder =
-        new ArtifactLocationBuilder().setRelativePath("com/google/Bla.java").setIsSource(true);
+    ArtifactLocation blah =
+        ArtifactLocation.builder().setRelativePath("com/google/Bla.java").setIsSource(true).build();
+    assertThat(decoder.decode(blah).getPath()).isEqualTo("/path/to/com/google/Bla.java");
 
-    assertThat(decoder.decode(builder.buildIdeInfoArtifact()).getRootPath())
-        .isEqualTo(packagePaths.get(0).toString());
+    ArtifactLocation foo =
+        ArtifactLocation.builder().setRelativePath("com/google/Foo.java").setIsSource(true).build();
+    assertThat(decoder.decode(foo).getPath())
+        .isEqualTo("/path/to/READONLY/root/com/google/Foo.java");
 
-    builder.setRelativePath("com/google/Foo.java");
+    ArtifactLocation test =
+        ArtifactLocation.builder().setRelativePath("com/other/Test.java").setIsSource(true).build();
+    assertThat(decoder.decode(test).getPath())
+        .isEqualTo("/path/to/CUSTOM/root/com/other/Test.java");
 
-    assertThat(decoder.decode(builder.buildIdeInfoArtifact()).getRootPath())
-        .isEqualTo(packagePaths.get(1).toString());
-
-    builder.setRelativePath("com/other/Test.java");
-
-    assertThat(decoder.decode(builder.buildIdeInfoArtifact()).getRootPath())
-        .isEqualTo(packagePaths.get(2).toString());
-
-    builder.setRelativePath("third_party/other/Temp.java");
-
-    assertThat(decoder.decode(builder.buildIdeInfoArtifact())).isNull();
+    ArtifactLocation.Builder temp =
+        ArtifactLocation.builder().setRelativePath("third_party/other/Temp.java").setIsSource(true);
+    assertThat(decoder.decode(temp.build()).getPath())
+        .isEqualTo("/path/to/third_party/other/Temp.java");
   }
 
   @Test
-  public void testDerivedArtifact() throws Exception {
-    ArtifactLocationBuilder builder =
-        new ArtifactLocationBuilder()
+  public void testGeneratedArtifact() throws Exception {
+    ArtifactLocation artifactLocation =
+        ArtifactLocation.builder()
             .setRootExecutionPathFragment("/blaze-out/bin")
             .setRelativePath("com/google/Bla.java")
-            .setIsSource(false);
-
-    ArtifactLocationDecoder decoder = new ArtifactLocationDecoder(BLAZE_CITC_ROOTS, null);
-
-    ArtifactLocation parsed = decoder.decode(builder.buildIdeInfoArtifact());
-
-    assertThat(parsed).isEqualTo(decoder.decode(builder.buildManifestArtifact()));
-
-    assertThat(parsed)
-        .isEqualTo(
-            ArtifactLocation.builder()
-                .setRootPath(EXECUTION_ROOT + "/blaze-out/bin")
-                .setRootExecutionPathFragment("/blaze-out/bin")
-                .setRelativePath("com/google/Bla.java")
-                .setIsSource(false)
-                .build());
-  }
-
-  @Test
-  public void testSourceArtifactAllVersions() throws Exception {
-    ArtifactLocationBuilder builder =
-        new ArtifactLocationBuilder().setRelativePath("com/google/Bla.java").setIsSource(true);
+            .setIsSource(false)
+            .build();
 
     ArtifactLocationDecoder decoder =
-        new ArtifactLocationDecoder(
-            BLAZE_CITC_ROOTS, new WorkspacePathResolverImpl(WORKSPACE_ROOT, BLAZE_CITC_ROOTS));
+        new ArtifactLocationDecoderImpl(
+            new BlazeRoots(
+                new File(EXECUTION_ROOT),
+                ImmutableList.of(new File("/path/to/root")),
+                new ExecutionRootPath("root/blaze-out/crosstool/bin"),
+                new ExecutionRootPath("root/blaze-out/crosstool/genfiles")),
+            null);
 
-    ArtifactLocation parsed = decoder.decode(builder.buildIdeInfoArtifact());
-
-    assertThat(parsed).isEqualTo(decoder.decode(builder.buildManifestArtifact()));
-
-    assertThat(parsed)
-        .isEqualTo(
-            ArtifactLocation.builder()
-                .setRootPath(WORKSPACE_ROOT.toString())
-                .setRelativePath("com/google/Bla.java")
-                .setIsSource(true)
-                .build());
-  }
-
-  static class ArtifactLocationBuilder {
-    String rootExecutionPathFragment = "";
-    String relativePath;
-    boolean isSource;
-
-    ArtifactLocationBuilder setRootExecutionPathFragment(String rootExecutionPathFragment) {
-      this.rootExecutionPathFragment = rootExecutionPathFragment;
-      return this;
-    }
-
-    ArtifactLocationBuilder setRelativePath(String relativePath) {
-      this.relativePath = relativePath;
-      return this;
-    }
-
-    ArtifactLocationBuilder setIsSource(boolean isSource) {
-      this.isSource = isSource;
-      return this;
-    }
-
-    AndroidStudioIdeInfo.ArtifactLocation buildIdeInfoArtifact() {
-      AndroidStudioIdeInfo.ArtifactLocation.Builder builder =
-          AndroidStudioIdeInfo.ArtifactLocation.newBuilder()
-              .setIsSource(isSource)
-              .setRelativePath(relativePath);
-      builder.setRootExecutionPathFragment(rootExecutionPathFragment);
-      return builder.build();
-    }
-
-    PackageManifestOuterClass.ArtifactLocation buildManifestArtifact() {
-      PackageManifestOuterClass.ArtifactLocation.Builder builder =
-          PackageManifestOuterClass.ArtifactLocation.newBuilder()
-              .setIsSource(isSource)
-              .setRelativePath(relativePath);
-      builder.setRootExecutionPathFragment(rootExecutionPathFragment);
-      return builder.build();
-    }
+    assertThat(decoder.decode(artifactLocation).getPath())
+        .isEqualTo(EXECUTION_ROOT + "/blaze-out/bin/com/google/Bla.java");
   }
 }
diff --git a/base/tests/utils/integration/com/google/idea/blaze/base/BlazeIntegrationTestCase.java b/base/tests/utils/integration/com/google/idea/blaze/base/BlazeIntegrationTestCase.java
index 06596be..98dc27d 100644
--- a/base/tests/utils/integration/com/google/idea/blaze/base/BlazeIntegrationTestCase.java
+++ b/base/tests/utils/integration/com/google/idea/blaze/base/BlazeIntegrationTestCase.java
@@ -30,24 +30,23 @@
 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.testing.EdtRule;
+import com.google.idea.testing.IntellijTestSetupRule;
+import com.google.idea.testing.ServiceHelper;
 import com.intellij.codeInsight.lookup.Lookup;
 import com.intellij.codeInsight.lookup.LookupElement;
 import com.intellij.codeInsight.lookup.LookupElementPresentation;
-import com.intellij.ide.plugins.PluginManagerCore;
+import com.intellij.openapi.Disposable;
 import com.intellij.openapi.actionSystem.IdeActions;
 import com.intellij.openapi.application.ApplicationManager;
-import com.intellij.openapi.application.PathManager;
 import com.intellij.openapi.application.ReadAction;
 import com.intellij.openapi.application.Result;
 import com.intellij.openapi.command.CommandProcessor;
 import com.intellij.openapi.editor.Editor;
 import com.intellij.openapi.editor.LogicalPosition;
-import com.intellij.openapi.extensions.ExtensionPoint;
 import com.intellij.openapi.extensions.ExtensionPointName;
-import com.intellij.openapi.extensions.Extensions;
 import com.intellij.openapi.project.Project;
 import com.intellij.openapi.projectRoots.ProjectJdkTable;
-import com.intellij.openapi.util.Disposer;
 import com.intellij.openapi.util.io.FileUtil;
 import com.intellij.openapi.vfs.VirtualFile;
 import com.intellij.openapi.vfs.ex.temp.TempFileSystem;
@@ -82,37 +81,25 @@
 import java.util.List;
 import java.util.stream.Collectors;
 import javax.annotation.Nullable;
-import org.picocontainer.MutablePicoContainer;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.rules.TestRule;
 
-/** Base test class for blaze integration tests. */
-public abstract class BlazeIntegrationTestCase extends UsefulTestCase {
+/** Base test class for blaze integration tests. {@link UsefulTestCase} */
+public abstract class BlazeIntegrationTestCase {
 
   private static final LightProjectDescriptor projectDescriptor =
       LightCodeInsightFixtureTestCase.JAVA_8;
 
-  private static boolean isRunThroughBlaze() {
-    return System.getenv("JAVA_RUNFILES") != null;
-  }
+  @Rule public final IntellijTestSetupRule setupRule = new IntellijTestSetupRule();
+  @Rule public final TestRule testRunWrapper = runTestsOnEdt() ? new EdtRule() : null;
 
   protected CodeInsightTestFixture testFixture;
   protected WorkspaceRoot workspaceRoot;
-  private String oldPluginPathProperty;
 
-  @Override
-  protected final void setUp() throws Exception {
-    if (!isRunThroughBlaze()) {
-      // If running directly through the IDE, don't try to load plugins from the sandbox environment
-      // Instead we'll rely on the slightly more hermetic module classpath
-      oldPluginPathProperty = System.getProperty(PathManager.PROPERTY_PLUGINS_PATH);
-      System.setProperty(PathManager.PROPERTY_PLUGINS_PATH, "/dev/null");
-    }
-
-    // Some plugins have a since-build and until-build restriction, so we need
-    // to update the build number here
-    PluginManagerCore.BUILD_NUMBER = "162.1447.26";
-
-    super.setUp();
-
+  @Before
+  public final void setUp() throws Exception {
     IdeaTestFixtureFactory factory = IdeaTestFixtureFactory.getFixtureFactory();
     TestFixtureBuilder<IdeaProjectTestFixture> fixtureBuilder =
         factory.createLightFixtureBuilder(projectDescriptor);
@@ -147,8 +134,10 @@
           }
           return vf.getInputStream();
         });
+  }
 
-    doSetup();
+  protected Disposable getTestRootDisposable() {
+    return setupRule.testRootDisposable;
   }
 
   /** Override to run tests with bazel specified as the project's build system. */
@@ -156,23 +145,16 @@
     return BuildSystem.Blaze;
   }
 
-  protected void doSetup() throws Exception {}
-
-  @Override
-  protected final void tearDown() throws Exception {
-    if (oldPluginPathProperty != null) {
-      System.setProperty(PathManager.PROPERTY_PLUGINS_PATH, oldPluginPathProperty);
-    } else {
-      System.clearProperty(PathManager.PROPERTY_PLUGINS_PATH);
-    }
-    testFixture.tearDown();
-    testFixture = null;
-    super.tearDown();
-    clearFields(this);
-    doTearDown();
+  /** Override to run tests off the EDT. */
+  protected boolean runTestsOnEdt() {
+    return true;
   }
 
-  protected void doTearDown() throws Exception {}
+  @After
+  public final void tearDown() throws Exception {
+    testFixture.tearDown();
+    testFixture = null;
+  }
 
   protected void setBlazeImportSettings(BlazeImportSettings importSettings) {
     BlazeImportSettingsManager.getInstance(getProject()).setImportSettings(importSettings);
@@ -435,36 +417,16 @@
   }
 
   protected <T> void registerApplicationService(Class<T> key, T implementation) {
-    registerComponentInstance(
-        (MutablePicoContainer) ApplicationManager.getApplication().getPicoContainer(),
-        key,
-        implementation);
+    ServiceHelper.registerApplicationService(key, implementation, getTestRootDisposable());
   }
 
   protected <T> void registerProjectService(Class<T> key, T implementation) {
-    registerComponentInstance(
-        (MutablePicoContainer) getProject().getPicoContainer(), key, implementation);
-  }
-
-  protected <T> void registerComponentInstance(
-      MutablePicoContainer container, Class<T> key, T implementation) {
-    Object old = container.getComponentInstance(key);
-    container.unregisterComponent(key.getName());
-    container.registerComponentInstance(key.getName(), implementation);
-    Disposer.register(
-        getTestRootDisposable(),
-        () -> {
-          container.unregisterComponent(key.getName());
-          if (old != null) {
-            container.registerComponentInstance(key.getName(), old);
-          }
-        });
+    ServiceHelper.registerProjectService(
+        getProject(), key, implementation, getTestRootDisposable());
   }
 
   protected <T> void registerExtension(ExtensionPointName<T> name, T instance) {
-    ExtensionPoint<T> ep = Extensions.getRootArea().getExtensionPoint(name);
-    ep.registerExtension(instance);
-    Disposer.register(getTestRootDisposable(), () -> ep.unregisterExtension(instance));
+    ServiceHelper.registerExtension(name, instance, getTestRootDisposable());
   }
 
   /** Redirects file system checks via the TempFileSystem used for these tests. */
diff --git a/base/tests/utils/integration/com/google/idea/blaze/base/lang/buildfile/BuildFileIntegrationTestCase.java b/base/tests/utils/integration/com/google/idea/blaze/base/lang/buildfile/BuildFileIntegrationTestCase.java
index fa91385..0dcea85 100644
--- a/base/tests/utils/integration/com/google/idea/blaze/base/lang/buildfile/BuildFileIntegrationTestCase.java
+++ b/base/tests/utils/integration/com/google/idea/blaze/base/lang/buildfile/BuildFileIntegrationTestCase.java
@@ -20,23 +20,27 @@
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableMap;
 import com.google.idea.blaze.base.BlazeIntegrationTestCase;
+import com.google.idea.blaze.base.ideinfo.RuleMap;
 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.BlazeProjectData;
-import com.google.idea.blaze.base.model.RuleMap;
 import com.google.idea.blaze.base.model.primitives.ExecutionRootPath;
+import com.google.idea.blaze.base.sync.workspace.ArtifactLocationDecoder;
+import com.google.idea.blaze.base.sync.workspace.ArtifactLocationDecoderImpl;
 import com.google.idea.blaze.base.sync.workspace.BlazeRoots;
 import com.google.idea.blaze.base.sync.workspace.WorkingSet;
+import com.google.idea.blaze.base.sync.workspace.WorkspacePathResolver;
 import com.google.idea.blaze.base.sync.workspace.WorkspacePathResolverImpl;
 import com.intellij.lang.ASTNode;
 import com.intellij.psi.PsiFile;
+import org.junit.Before;
 
 /** BUILD file specific integration test base */
 public abstract class BuildFileIntegrationTestCase extends BlazeIntegrationTestCase {
 
-  @Override
-  protected void doSetup() {
+  @Before
+  public final void doSetup() {
     mockBlazeProjectDataManager(getMockBlazeProjectData());
   }
 
@@ -67,12 +71,17 @@
             ImmutableList.of(workspaceRoot.directory()),
             new ExecutionRootPath("out/crosstool/bin"),
             new ExecutionRootPath("out/crosstool/gen"));
+    WorkspacePathResolver workspacePathResolver =
+        new WorkspacePathResolverImpl(workspaceRoot, fakeRoots);
+    ArtifactLocationDecoder artifactLocationDecoder =
+        new ArtifactLocationDecoderImpl(fakeRoots, workspacePathResolver);
     return new BlazeProjectData(
         0,
         new RuleMap(ImmutableMap.of()),
         fakeRoots,
         new WorkingSet(ImmutableList.of(), ImmutableList.of(), ImmutableList.of()),
-        new WorkspacePathResolverImpl(workspaceRoot, fakeRoots),
+        workspacePathResolver,
+        artifactLocationDecoder,
         null,
         null,
         null,
diff --git a/base/tests/utils/integration/com/google/idea/blaze/base/sync/BlazeSyncIntegrationTestCase.java b/base/tests/utils/integration/com/google/idea/blaze/base/sync/BlazeSyncIntegrationTestCase.java
index 180e1cc..fd78f8c 100644
--- a/base/tests/utils/integration/com/google/idea/blaze/base/sync/BlazeSyncIntegrationTestCase.java
+++ b/base/tests/utils/integration/com/google/idea/blaze/base/sync/BlazeSyncIntegrationTestCase.java
@@ -28,8 +28,8 @@
 import com.google.idea.blaze.base.BlazeIntegrationTestCase;
 import com.google.idea.blaze.base.command.info.BlazeInfo;
 import com.google.idea.blaze.base.ideinfo.ArtifactLocation;
+import com.google.idea.blaze.base.ideinfo.RuleMap;
 import com.google.idea.blaze.base.io.WorkspaceScanner;
-import com.google.idea.blaze.base.model.RuleMap;
 import com.google.idea.blaze.base.model.SyncState;
 import com.google.idea.blaze.base.model.primitives.TargetExpression;
 import com.google.idea.blaze.base.model.primitives.WorkspacePath;
@@ -64,6 +64,8 @@
 import java.util.List;
 import java.util.Map;
 import javax.annotation.Nullable;
+import org.junit.After;
+import org.junit.Before;
 
 /** Sets up mocks required for integration tests of the blaze sync process. */
 public abstract class BlazeSyncIntegrationTestCase extends BlazeIntegrationTestCase {
@@ -89,10 +91,11 @@
   protected ErrorCollector errorCollector;
   protected BlazeContext context;
 
-  @Override
-  protected void doSetup() throws IOException {
+  @Before
+  public void doSetup() throws Exception {
     // Set up a workspace root outside of the tracked temp file system.
     tempDirectoryHandler = new LightTempDirTestFixtureImpl();
+    tempDirectoryHandler.setUp();
     tempDirectory = tempDirectoryHandler.getFile("");
     workspaceRoot = new WorkspaceRoot(new File(tempDirectory.getPath()));
     setBlazeImportSettings(
@@ -125,7 +128,7 @@
                 // don't commit module changes,
                 // but make sure they're properly disposed when the test is finished
                 for (ModifiableRootModel model : modifiableModels) {
-                  Disposer.register(myTestRootDisposable, model::dispose);
+                  Disposer.register(getTestRootDisposable(), model::dispose);
                 }
               }
             };
@@ -150,12 +153,11 @@
             workspaceRoot.toString()));
   }
 
-  @Override
-  protected void doTearDown() throws Exception {
+  @After
+  public final void doTearDown() throws Exception {
     if (tempDirectoryHandler != null) {
       tempDirectoryHandler.tearDown();
     }
-    super.doTearDown();
   }
 
   protected VirtualFile createWorkspaceFile(String relativePath, @Nullable String... contents) {
@@ -173,7 +175,6 @@
 
   protected ArtifactLocation sourceRoot(String relativePath) {
     return ArtifactLocation.builder()
-        .setRootPath(workspaceRoot.toString())
         .setRelativePath(relativePath)
         .setIsSource(true)
         .build();
diff --git a/base/tests/utils/unit/com/google/idea/blaze/base/ideinfo/RuleMapBuilder.java b/base/tests/utils/unit/com/google/idea/blaze/base/ideinfo/RuleMapBuilder.java
index 2586377..2b82cd9 100644
--- a/base/tests/utils/unit/com/google/idea/blaze/base/ideinfo/RuleMapBuilder.java
+++ b/base/tests/utils/unit/com/google/idea/blaze/base/ideinfo/RuleMapBuilder.java
@@ -17,8 +17,6 @@
 
 import com.google.common.collect.ImmutableMap;
 import com.google.common.collect.Lists;
-import com.google.idea.blaze.base.model.RuleMap;
-import com.google.idea.blaze.base.model.primitives.Label;
 import java.util.List;
 import org.jetbrains.annotations.NotNull;
 
@@ -43,9 +41,10 @@
 
   @NotNull
   public RuleMap build() {
-    ImmutableMap.Builder<Label, RuleIdeInfo> ruleMap = ImmutableMap.builder();
+    ImmutableMap.Builder<RuleKey, RuleIdeInfo> ruleMap = ImmutableMap.builder();
     for (RuleIdeInfo rule : rules) {
-      ruleMap.put(rule.label, rule);
+      RuleKey key = rule.key;
+      ruleMap.put(key, rule);
     }
     return new RuleMap(ruleMap.build());
   }
diff --git a/base/tests/utils/unit/com/google/idea/blaze/base/run/MockBlazeCommandRunConfigurationHandlerProvider.java b/base/tests/utils/unit/com/google/idea/blaze/base/run/MockBlazeCommandRunConfigurationHandlerProvider.java
index 92ab14e..4f30237 100644
--- a/base/tests/utils/unit/com/google/idea/blaze/base/run/MockBlazeCommandRunConfigurationHandlerProvider.java
+++ b/base/tests/utils/unit/com/google/idea/blaze/base/run/MockBlazeCommandRunConfigurationHandlerProvider.java
@@ -17,15 +17,17 @@
 
 import com.google.idea.blaze.base.model.primitives.Kind;
 import com.google.idea.blaze.base.run.confighandler.BlazeCommandRunConfigurationHandler;
-import com.google.idea.blaze.base.run.confighandler.BlazeCommandRunConfigurationHandlerEditor;
 import com.google.idea.blaze.base.run.confighandler.BlazeCommandRunConfigurationHandlerProvider;
+import com.google.idea.blaze.base.run.confighandler.BlazeCommandRunConfigurationRunner;
+import com.google.idea.blaze.base.run.state.RunConfigurationState;
+import com.google.idea.blaze.base.run.state.RunConfigurationStateEditor;
 import com.intellij.execution.ExecutionException;
 import com.intellij.execution.Executor;
 import com.intellij.execution.configurations.RunConfiguration;
 import com.intellij.execution.configurations.RunProfileState;
 import com.intellij.execution.configurations.RuntimeConfigurationException;
 import com.intellij.execution.runners.ExecutionEnvironment;
-import com.intellij.openapi.util.InvalidDataException;
+import com.intellij.openapi.project.Project;
 import javax.annotation.Nullable;
 import javax.swing.Icon;
 import javax.swing.JComponent;
@@ -49,23 +51,11 @@
     return "MockBlazeCommandRunConfigurationHandlerProvider";
   }
 
-  /** A mock {@link BlazeCommandRunConfigurationHandler}. */
-  private static class MockBlazeCommandRunConfigurationHandler
-      implements BlazeCommandRunConfigurationHandler {
-
-    final BlazeCommandRunConfiguration configuration;
-
-    MockBlazeCommandRunConfigurationHandler(BlazeCommandRunConfiguration configuration) {
-      this.configuration = configuration;
-    }
+  /** A mock {@link RunConfigurationState}. */
+  private static class MockRunConfigurationState implements RunConfigurationState {
 
     @Override
-    public void checkConfiguration() throws RuntimeConfigurationException {
-      // Don't throw anything.
-    }
-
-    @Override
-    public void readExternal(Element element) throws InvalidDataException {
+    public void readExternal(Element element) {
       // Don't read anything.
     }
 
@@ -75,13 +65,32 @@
     }
 
     @Override
-    public BlazeCommandRunConfigurationHandler cloneFor(
-        BlazeCommandRunConfiguration configuration) {
-      return new MockBlazeCommandRunConfigurationHandler(configuration);
+    public RunConfigurationStateEditor getEditor(Project project) {
+      return new RunConfigurationStateEditor() {
+        @Override
+        public void resetEditorFrom(RunConfigurationState state) {
+          // Do nothing.
+        }
+
+        @Override
+        public void applyEditorTo(RunConfigurationState state) {
+          // Do nothing.
+        }
+
+        @Override
+        public JComponent createComponent() {
+          return null;
+        }
+      };
     }
+  }
+
+  /** A mock {@link MockBlazeCommandRunConfigurationRunner}. */
+  private static class MockBlazeCommandRunConfigurationRunner
+      implements BlazeCommandRunConfigurationRunner {
 
     @Override
-    public RunProfileState getState(Executor executor, ExecutionEnvironment environment)
+    public RunProfileState getRunProfileState(Executor executor, ExecutionEnvironment environment)
         throws ExecutionException {
       return null;
     }
@@ -90,16 +99,40 @@
     public boolean executeBeforeRunTask(ExecutionEnvironment environment) {
       return true;
     }
+  }
 
-    @Nullable
-    @Override
-    public String suggestedName() {
-      return null;
+  /** A mock {@link BlazeCommandRunConfigurationHandler}. */
+  private static class MockBlazeCommandRunConfigurationHandler
+      implements BlazeCommandRunConfigurationHandler {
+
+    final BlazeCommandRunConfiguration configuration;
+    final MockRunConfigurationState state;
+
+    MockBlazeCommandRunConfigurationHandler(BlazeCommandRunConfiguration configuration) {
+      this.configuration = configuration;
+      this.state = new MockRunConfigurationState();
     }
 
     @Override
-    public boolean isGeneratedName(boolean hasGeneratedFlag) {
-      return hasGeneratedFlag;
+    public MockRunConfigurationState getState() {
+      return state;
+    }
+
+    @Override
+    public BlazeCommandRunConfigurationRunner createRunner(
+        Executor executor, ExecutionEnvironment environment) {
+      return new MockBlazeCommandRunConfigurationRunner();
+    }
+
+    @Override
+    public void checkConfiguration() throws RuntimeConfigurationException {
+      // Don't throw anything.
+    }
+
+    @Nullable
+    @Override
+    public String suggestedName(BlazeCommandRunConfiguration configuration) {
+      return null;
     }
 
     @Nullable
@@ -118,26 +151,5 @@
     public Icon getExecutorIcon(RunConfiguration configuration, Executor executor) {
       return null;
     }
-
-    @Override
-    public BlazeCommandRunConfigurationHandlerEditor getHandlerEditor() {
-      return new BlazeCommandRunConfigurationHandlerEditor() {
-        @Override
-        public void resetEditorFrom(BlazeCommandRunConfigurationHandler handler) {
-          // Do nothing.
-        }
-
-        @Override
-        public void applyEditorTo(BlazeCommandRunConfigurationHandler handler) {
-          // Do nothing.
-        }
-
-        @Nullable
-        @Override
-        public JComponent createEditor() {
-          return null;
-        }
-      };
-    }
   }
 }
diff --git a/build_defs/build_defs.bzl b/build_defs/build_defs.bzl
index 974b857..d8c9613 100644
--- a/build_defs/build_defs.bzl
+++ b/build_defs/build_defs.bzl
@@ -5,6 +5,7 @@
      "merged_plugin_xml_impl",
      "stamped_plugin_xml_impl",
      "product_build_txt_impl",
+     "api_version_txt_impl",
      "intellij_plugin_impl",
      "plugin_bundle_impl")
 
@@ -25,6 +26,8 @@
                        include_product_code_in_stamp=False,
                        version_file=None,
                        changelog_file=None,
+                       description_file=None,
+                       vendor_file=None,
                        **kwargs):
   """Stamps a plugin xml file with the IJ build number.
 
@@ -39,12 +42,16 @@
       is included in since-build and until-build.
     version_file: A file with the version number to be included.
     changelog_file: A file with changelog to be included.
+    description_file: A file containing a plugin description to be included.
+    vendor_file: A file containing the vendor info to be included.
     **kwargs: Any additional arguments to pass to the final target.
   """
+  api_version_txt(
+      name = name + "_api_version",
+  )
   stamped_plugin_xml_impl(
       name = name,
-      application_info_jar = "//intellij_platform_sdk:application_info_jar",
-      application_info_name = "//intellij_platform_sdk:application_info_name",
+      api_version_txt = name + "_api_version",
       plugin_id = plugin_id,
       plugin_name = plugin_name,
       stamp_tool = "//build_defs/shared:stamp_plugin_xml",
@@ -54,6 +61,8 @@
       include_product_code_in_stamp = include_product_code_in_stamp,
       version_file = version_file,
       changelog_file = changelog_file,
+      description_file = description_file,
+      vendor_file = vendor_file,
       **kwargs)
 
 def product_build_txt(name, **kwargs):
@@ -65,6 +74,14 @@
       product_build_txt_tool = "//build_defs/shared:product_build_txt",
       **kwargs)
 
+def api_version_txt(name, **kwargs):
+  """Produces an api_version.txt file with the api version, including the product code."""
+  api_version_txt_impl(
+      name = name,
+      application_info_jar = "//intellij_platform_sdk:application_info_jar",
+      application_info_name = "//intellij_platform_sdk:application_info_name",
+      api_version_txt_tool = "//build_defs/shared:api_version_txt",
+      **kwargs)
 
 def intellij_plugin(name, plugin_xml, deps, meta_inf_files=[], **kwargs):
   """Creates an intellij plugin from the given deps and plugin.xml."""
diff --git a/build_defs/shared/BUILD b/build_defs/shared/BUILD
index bb4815c..3c8b0b3 100644
--- a/build_defs/shared/BUILD
+++ b/build_defs/shared/BUILD
@@ -20,3 +20,8 @@
     name = "product_build_txt",
     srcs = ["product_build_txt.py"],
 )
+
+py_binary(
+    name = "api_version_txt",
+    srcs = ["api_version_txt.py"],
+)
diff --git a/build_defs/shared/api_version_txt.py b/build_defs/shared/api_version_txt.py
new file mode 100755
index 0000000..a1250c5
--- /dev/null
+++ b/build_defs/shared/api_version_txt.py
@@ -0,0 +1,74 @@
+"""Produces a api_version.txt file with the plugin API version.
+"""
+
+import argparse
+import re
+from xml.dom.minidom import parseString
+import zipfile
+
+parser = argparse.ArgumentParser()
+
+parser.add_argument(
+    "--application_info_jar",
+    help="The jar file containing the application info xml",
+    required=True,)
+parser.add_argument(
+    "--application_info_name",
+    help="A .txt file containing the application info xml name",
+    required=True,)
+
+
+def _parse_build_number(build_number):
+  """Parses the build number.
+
+  Args:
+    build_number: The build number as text.
+  Returns:
+    build_number, build_number_without_product_code.
+  Raises:
+    ValueError: if the build number is invalid.
+  """
+  match = re.match(r"^([A-Z]+-)?([0-9]+)((\.[0-9]+)*)", build_number)
+  if match is None:
+    raise ValueError("Invalid build number: " + build_number)
+
+  return match.group(1) + match.group(2) + match.group(3)
+
+
+def main():
+
+  args = parser.parse_args()
+
+  with open(args.application_info_name) as f:
+    application_info_name = f.read().strip()
+
+  with zipfile.ZipFile(args.application_info_jar, "r") as zf:
+    try:
+      data = zf.read(application_info_name)
+    except:
+      raise ValueError("Could not read application info file: " +
+                       application_info_name)
+    component = parseString(data)
+
+    build_elements = component.getElementsByTagName("build")
+    if not build_elements:
+      raise ValueError("Could not find <build> element.")
+    if len(build_elements) > 1:
+      raise ValueError("Ambiguous <build> element.")
+    build_element = build_elements[0]
+
+    attrs = build_element.attributes
+    if attrs.has_key("apiVersion"):
+      api_version_attr = attrs.get("apiVersion")
+    else:
+      api_version_attr = attrs.get("number")
+
+  if not api_version_attr:
+    raise ValueError("Could not find api version in application info")
+
+  api_version = _parse_build_number(api_version_attr.value)
+  print api_version
+
+
+if __name__ == "__main__":
+  main()
diff --git a/build_defs/shared/build_defs.bzl b/build_defs/shared/build_defs.bzl
index 5c282ee..5726ed0 100644
--- a/build_defs/shared/build_defs.bzl
+++ b/build_defs/shared/build_defs.bzl
@@ -18,8 +18,7 @@
 
 def stamped_plugin_xml_impl(name,
                             plugin_xml,
-                            application_info_jar,
-                            application_info_name,
+                            api_version_txt,
                             stamp_tool,
                             plugin_id = None,
                             plugin_name = None,
@@ -28,14 +27,15 @@
                             include_product_code_in_stamp=False,
                             version_file=None,
                             changelog_file=None,
+                            description_file=None,
+                            vendor_file=None,
                             **kwargs):
   """Stamps a plugin xml file with the IJ build number.
 
   Args:
     name: name of this target
     plugin_xml: target plugin_xml to stamp
-    application_info_jar: the jar containing the application info
-    application_info_name: a file with the name of the application info
+    api_version_txt: the file containing the api version
     stamp_tool: the tool to use to stamp the version
     plugin_id: the plugin ID to stamp
     plugin_name: the plugin name to stamp
@@ -45,18 +45,19 @@
         is included in since-build and until-build.
     version_file: A file with the version number to be included.
     changelog_file: A file with the changelog to be included.
+    description_file: A file containing a plugin description to be included.
+    vendor_file: A file containing the vendor info to be included.
     **kwargs: Any additional arguments to pass to the final target.
   """
   args = [
       "./$(location {stamp_tool})",
       "--plugin_xml=$(location {plugin_xml})",
-      "--application_info_jar=$(location {application_info_jar})",
-      "--application_info_name=$(location {application_info_name})",
+      "--api_version_txt=$(location {api_version_txt})",
       "{stamp_since_build}",
       "{stamp_until_build}",
       "{include_product_code_in_stamp}",
   ]
-  srcs = [plugin_xml, application_info_jar, application_info_name]
+  srcs = [plugin_xml, api_version_txt]
 
   if plugin_id:
     args.append("--plugin_id=%s" % plugin_id)
@@ -72,10 +73,17 @@
     args.append("--changelog_file=$(location {changelog_file})")
     srcs.append(changelog_file)
 
+  if description_file:
+    args.append("--description_file=$(location {description_file})")
+    srcs.append(description_file)
+
+  if vendor_file:
+    args.append("--vendor_file=$(location {vendor_file})")
+    srcs.append(vendor_file)
+
   cmd = " ".join(args).format(
       plugin_xml=plugin_xml,
-      application_info_jar=application_info_jar,
-      application_info_name=application_info_name,
+      api_version_txt=api_version_txt,
       stamp_tool=stamp_tool,
       stamp_since_build=_optstr("stamp_since_build",
                                 stamp_since_build),
@@ -86,6 +94,8 @@
           include_product_code_in_stamp),
       version_file=version_file,
       changelog_file=changelog_file,
+      description_file=description_file,
+      vendor_file=vendor_file,
   ) + "> $@"
 
   native.genrule(
@@ -128,6 +138,38 @@
       tools = [product_build_txt_tool],
       **kwargs)
 
+def api_version_txt_impl(name,
+                         api_version_txt_tool,
+                         application_info_jar,
+                         application_info_name,
+                         **kwargs):
+  """Produces an api_version.txt file with the api version, including the product code.
+
+  Args:
+    name: name of this target
+    api_version_txt_tool: the api version tool
+    application_info_jar: the jar containing the application info
+    application_info_name: a file with the name of the application info
+    **kwargs: Any additional arguments to pass to the final target.
+  """
+  args = [
+      "./$(location {api_version_txt_tool})",
+      "--application_info_jar=$(location {application_info_jar})",
+      "--application_info_name=$(location {application_info_name})",
+  ]
+  cmd = " ".join(args).format(
+      application_info_jar=application_info_jar,
+      application_info_name=application_info_name,
+      api_version_txt_tool=api_version_txt_tool,
+  ) + "> $@"
+  native.genrule(
+      name = name,
+      srcs = [application_info_jar, application_info_name],
+      outs = [name + ".txt"],
+      cmd = cmd,
+      tools = [api_version_txt_tool],
+      **kwargs)
+
 def intellij_plugin_impl(name,
                          deps,
                          plugin_xml,
diff --git a/build_defs/shared/stamp_plugin_xml.py b/build_defs/shared/stamp_plugin_xml.py
index b4fdfb6..aeaf5d2 100755
--- a/build_defs/shared/stamp_plugin_xml.py
+++ b/build_defs/shared/stamp_plugin_xml.py
@@ -3,8 +3,6 @@
 import argparse
 import re
 from xml.dom.minidom import parse
-from xml.dom.minidom import parseString
-import zipfile
 
 parser = argparse.ArgumentParser()
 
@@ -14,13 +12,8 @@
     required=True,
 )
 parser.add_argument(
-    "--application_info_jar",
-    help="The jar file containing the application info xml",
-    required=True,
-)
-parser.add_argument(
-    "--application_info_name",
-    help="A .txt file containing the application info xml name",
+    "--api_version_txt",
+    help="The file containing the api version info",
     required=True,
 )
 parser.add_argument(
@@ -50,6 +43,14 @@
     help="Changelog file to add to plugin.xml",
 )
 parser.add_argument(
+    "--description_file",
+    help="File with description element data to add to plugin.xml",
+)
+parser.add_argument(
+    "--vendor_file",
+    help="File with vendor element data to add to plugin.xml",
+)
+parser.add_argument(
     "--include_product_code_in_stamp",
     action="store_true",
     help="Include the product code in the stamp",
@@ -62,60 +63,38 @@
     return "\n".join("<p>" + line + "</p>" for line in f.readlines())
 
 
-def _parse_build_number(build_number):
-  """Parses the build number.
+def _read_description(description_file):
+  """Reads the description and transforms it into trivial HTML."""
+  with open(description_file) as f:
+    return "\n".join("<p>" + line + "</p>" for line in f.readlines())
 
-  Args:
-    build_number: The build number as text.
-  Returns:
-    build_number, build_number_without_product_code.
-  Raises:
-    ValueError: if the build number is invalid.
-  """
-  match = re.match(r"^([A-Z]+-)?([0-9]+)(\.[0-9]+)?", build_number)
+
+def _read_vendor(vendor_file):
+  """Reads vendor data from an .xml file and returns the vendor element."""
+  dom = parse(vendor_file)
+  vendor_elements = dom.getElementsByTagName("vendor")
+  if len(vendor_elements) != 1:
+    raise ValueError("Ambigious or missing vendor element (%d elements)" %
+                     len(vendor_elements))
+  return vendor_elements[0]
+
+
+def _strip_product_code(api_version):
+  """Strips the product code from the api version string."""
+  match = re.match(r"^([A-Z]+-)?([0-9]+)((\.[0-9]+)*)", api_version)
   if match is None:
-    raise ValueError("Invalid build number: " + build_number)
+    raise ValueError("Invalid build number: " + api_version)
 
-  build_number = match.group(1) + match.group(2) + match.group(3)
-  build_number_without_product_code = match.group(2) + match.group(3)
-  return build_number, build_number_without_product_code
+  return match.group(2) + match.group(3)
 
 
 def main():
-
   args = parser.parse_args()
 
   dom = parse(args.plugin_xml)
 
-  with open(args.application_info_name) as f:
-    application_info_name = f.read().strip()
-
-  with zipfile.ZipFile(args.application_info_jar, "r") as zf:
-    try:
-      data = zf.read(application_info_name)
-    except:
-      raise ValueError("Could not read application info file: " +
-                       application_info_name)
-    component = parseString(data)
-
-    build_elements = component.getElementsByTagName("build")
-    if not build_elements:
-      raise ValueError("Could not find <build> element.")
-    if len(build_elements) > 1:
-      raise ValueError("Ambiguous <build> element.")
-    build_element = build_elements[0]
-
-    attrs = build_element.attributes
-    if attrs.has_key("apiVersion"):
-      api_version_attr = attrs.get("apiVersion")
-    else:
-      api_version_attr = attrs.get("number")
-
-  if not api_version_attr:
-    raise ValueError("Could not find api version in application info")
-
-  api_version, api_version_without_product_code = _parse_build_number(
-      api_version_attr.value)
+  with open(args.api_version_txt) as f:
+    api_version = f.readline().strip()
 
   new_elements = []
 
@@ -145,7 +124,7 @@
 
     idea_version_build_element = (api_version
                                   if args.include_product_code_in_stamp else
-                                  api_version_without_product_code)
+                                  _strip_product_code(api_version))
 
     idea_version_element = dom.createElement("idea-version")
     new_elements.append(idea_version_element)
@@ -182,6 +161,27 @@
     name_text = dom.createTextNode(args.plugin_name)
     name_element.appendChild(name_text)
 
+  if args.description_file:
+    if idea_plugin.getElementsByTagName("description"):
+      raise ValueError("description element already in plugin.xml")
+    description_element = dom.createElement("description")
+    description_text = _read_description(args.description_file)
+    description_cdata = dom.createCDATASection(description_text)
+    description_element.appendChild(description_cdata)
+    new_elements.append(description_element)
+
+  if args.vendor_file:
+    if idea_plugin.getElementsByTagName("vendor"):
+      raise ValueError("vendor element already in plugin.xml")
+    vendor_element = dom.createElement("vendor")
+    vendor_src_element = _read_vendor(args.vendor_file)
+    vendor_element.setAttribute("email",
+                                vendor_src_element.getAttribute("email"))
+    vendor_element.setAttribute("url", vendor_src_element.getAttribute("url"))
+    vendor_text = dom.createTextNode(vendor_src_element.firstChild.data)
+    vendor_element.appendChild(vendor_text)
+    new_elements.append(vendor_element)
+
   for new_element in new_elements:
     idea_plugin.appendChild(new_element)
 
diff --git a/clwb/BUILD b/clwb/BUILD
new file mode 100644
index 0000000..792d459
--- /dev/null
+++ b/clwb/BUILD
@@ -0,0 +1,61 @@
+#
+# Description: Builds clwb
+#
+
+licenses(["notice"])  # Apache 2.0
+
+load(
+    "//build_defs:build_defs.bzl",
+    "intellij_plugin",
+    "merged_plugin_xml",
+    "stamped_plugin_xml",
+)
+
+merged_plugin_xml(
+    name = "merged_plugin_xml_common",
+    srcs = [
+        "src/META-INF/clwb.xml",
+        "//base:plugin_xml",
+        "//cpp:plugin_xml",
+    ],
+    visibility = ["//visibility:public"],
+)
+
+merged_plugin_xml(
+    name = "merged_plugin_xml",
+    srcs = [
+        "src/META-INF/clwb_bazel.xml",
+        ":merged_plugin_xml_common",
+    ],
+)
+
+stamped_plugin_xml(
+    name = "stamped_plugin_xml",
+    include_product_code_in_stamp = True,
+    plugin_id = "com.google.idea.bazel.clwb",
+    plugin_name = "CLion with Bazel",
+    plugin_xml = ":merged_plugin_xml",
+    stamp_since_build = True,
+    version_file = "//:version",
+)
+
+java_library(
+    name = "clwb_lib",
+    srcs = glob(["src/**/*.java"]),
+    visibility = ["//visibility:public"],
+    deps = [
+        "//base",
+        "//common/experiments",
+        "//cpp",
+        "//intellij_platform_sdk:plugin_api",
+        "@jsr305_annotations//jar",
+    ],
+)
+
+intellij_plugin(
+    name = "clwb_bazel",
+    plugin_xml = ":stamped_plugin_xml",
+    deps = [
+        ":clwb_lib",
+    ],
+)
diff --git a/clwb/clwb.bazelproject b/clwb/clwb.bazelproject
new file mode 100644
index 0000000..76911c2
--- /dev/null
+++ b/clwb/clwb.bazelproject
@@ -0,0 +1,19 @@
+directories:
+  .
+  -ijwb
+  -aswb
+  -plugin_dev
+  -cpp/src/com/google/idea/blaze/cpp/versioned/v145
+
+targets:
+  //clwb:clwb_bazel
+  //:clwb_tests
+
+workspace_type: intellij_plugin
+
+build_flags:
+  --define=ij_product=clion-latest
+
+test_sources:
+  */tests/unittests*
+  */tests/integrationtests*
diff --git a/clwb/src/META-INF/clwb.xml b/clwb/src/META-INF/clwb.xml
new file mode 100644
index 0000000..4cc92ef
--- /dev/null
+++ b/clwb/src/META-INF/clwb.xml
@@ -0,0 +1,73 @@
+<!--
+  ~ 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>
+  <vendor>Google</vendor>
+
+  <depends>com.intellij.modules.clion</depends>
+
+  <extensions defaultExtensionNs="com.intellij">
+    <applicationService serviceInterface="com.google.idea.blaze.base.plugin.BlazePluginId"
+                        serviceImplementation="com.google.idea.blaze.plugin.ClwbPluginId"/>
+    <projectService serviceInterface="com.google.idea.blaze.base.ui.BlazeProblemsView"
+                    serviceImplementation="com.google.idea.blaze.clwb.problemsview.BlazeProblemsViewConsole"/>
+    <toolWindow id="Blaze Problems View"
+                anchor="bottom"
+                secondary="true"
+                conditionClass="com.google.idea.blaze.base.settings.IsBlazeProjectCondition"
+                icon="BlazeIcons.BlazeToolWindow"
+                factoryClass="com.google.idea.blaze.clwb.problemsview.BlazeProblemsViewConsoleToolWindowFactory"/>
+    <projectService serviceInterface="com.jetbrains.cidr.lang.workspace.OCWorkspaceManager"
+                    serviceImplementation="com.google.idea.blaze.clwb.cworkspace.BlazeCWorkspaceManager"
+                    overrides="true"/>
+
+    <!-- run configurations -->
+    <programRunner implementation="com.google.idea.blaze.clwb.run.BlazeCppRunner"/>
+
+    <runConfigurationProducer
+        implementation="com.google.idea.blaze.clwb.run.producers.BlazeCidrTestConfigurationProducer"
+        order="first"/>
+    <!-- end run configurations -->
+  </extensions>
+
+  <extensions defaultExtensionNs="cidr.debugger">
+    <languageSupportFactory implementation="com.google.idea.blaze.clwb.run.BlazeCidrDebuggerSupportFactory"/>
+  </extensions>
+
+  <extensions defaultExtensionNs="com.google.idea.blaze">
+    <SyncPlugin implementation="com.google.idea.blaze.clwb.sync.BlazeCLionSyncPlugin"/>
+    <BlazeCommandRunConfigurationHandlerProvider implementation="com.google.idea.blaze.clwb.run.BlazeCidrRunConfigurationHandlerProvider" order="first"/>
+  </extensions>
+
+  <actions>
+    <action id="Blaze.ImportProject2" class="com.google.idea.blaze.clwb.wizard2.BlazeImportProjectAction" icon="BlazeIcons.Blaze">
+      <add-to-group group-id="WelcomeScreen.QuickStart"/>
+      <add-to-group group-id="FileOpenGroup" anchor="first"/>
+    </action>
+  </actions>
+
+  <application-components>
+    <component>
+      <implementation-class>com.google.idea.blaze.plugin.ClwbSpecificInitializer</implementation-class>
+    </component>
+  </application-components>
+
+  <project-components>
+    <component>
+      <implementation-class>com.google.idea.blaze.plugin.ClwbProjectSpecificInitializer</implementation-class>
+    </component>
+  </project-components>
+
+</idea-plugin>
diff --git a/clwb/src/META-INF/clwb_bazel.xml b/clwb/src/META-INF/clwb_bazel.xml
new file mode 100644
index 0000000..9bd8aff
--- /dev/null
+++ b/clwb/src/META-INF/clwb_bazel.xml
@@ -0,0 +1,31 @@
+<!--
+  ~ 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>
+  <description>
+    <![CDATA[
+      <a href="http://bazel.io">Bazel</a> support for CLion.
+
+      Features:
+        <ul>
+        <li>Import BUILD files into the IDE.</li>
+        <li>BUILD file custom language support.</li>
+        <li>Support for Bazel run configurations for certain rule classes.</li>
+        </ul>
+
+      Usage instructions at <a href="http://ij.bazel.io">ij.bazel.io</a>
+      ]]>
+  </description>
+</idea-plugin>
\ No newline at end of file
diff --git a/clwb/src/com/google/idea/blaze/clwb/cworkspace/BlazeCWorkspaceManager.java b/clwb/src/com/google/idea/blaze/clwb/cworkspace/BlazeCWorkspaceManager.java
new file mode 100644
index 0000000..fe6c961
--- /dev/null
+++ b/clwb/src/com/google/idea/blaze/clwb/cworkspace/BlazeCWorkspaceManager.java
@@ -0,0 +1,44 @@
+/*
+ * Copyright 2016 The Bazel Authors. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.idea.blaze.clwb.cworkspace;
+
+import com.google.idea.blaze.base.settings.Blaze;
+import com.google.idea.blaze.cpp.BlazeCWorkspace;
+import com.intellij.openapi.project.Project;
+import com.jetbrains.cidr.lang.CPPWorkspaceManager;
+import com.jetbrains.cidr.lang.workspace.OCWorkspace;
+import com.jetbrains.cidr.lang.workspace.OCWorkspaceManager;
+
+class BlazeCWorkspaceManager extends OCWorkspaceManager {
+  private final Project project;
+  private final OCWorkspaceManager delegate;
+
+  public BlazeCWorkspaceManager(Project project) {
+    this.project = project;
+    this.delegate = new CPPWorkspaceManager(project);
+  }
+
+  @Override
+  public OCWorkspace getWorkspace() {
+    if (Blaze.isBlazeProject(project)) {
+      return BlazeCWorkspace.getInstance(project);
+    }
+    // this is a gross hack, necessitated by OCWorkspaceManager being a service, rather than
+    // using extension points. We don't actually know which OCWorkspaceManager would be used
+    // if this one wasn't overriding -- but we'll guess that it was CPPWorkspaceManager...
+    return delegate.getWorkspace();
+  }
+}
diff --git a/clwb/src/com/google/idea/blaze/clwb/problemsview/BlazeProblemsViewConsole.java b/clwb/src/com/google/idea/blaze/clwb/problemsview/BlazeProblemsViewConsole.java
new file mode 100644
index 0000000..07703e3
--- /dev/null
+++ b/clwb/src/com/google/idea/blaze/clwb/problemsview/BlazeProblemsViewConsole.java
@@ -0,0 +1,254 @@
+/*
+ * Copyright 2016 The Bazel Authors. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.idea.blaze.clwb.problemsview;
+
+import com.google.idea.blaze.base.scope.output.IssueOutput;
+import com.google.idea.blaze.base.ui.BlazeProblemsView;
+import com.intellij.icons.AllIcons;
+import com.intellij.ide.errorTreeView.ErrorTreeElement;
+import com.intellij.ide.errorTreeView.ErrorTreeElementKind;
+import com.intellij.ide.errorTreeView.ErrorViewStructure;
+import com.intellij.ide.errorTreeView.GroupingElement;
+import com.intellij.openapi.components.ServiceManager;
+import com.intellij.openapi.diagnostic.Logger;
+import com.intellij.openapi.fileEditor.OpenFileDescriptor;
+import com.intellij.openapi.project.Project;
+import com.intellij.openapi.util.Disposer;
+import com.intellij.openapi.util.IconLoader;
+import com.intellij.openapi.vfs.VfsUtil;
+import com.intellij.openapi.vfs.VirtualFile;
+import com.intellij.openapi.wm.ToolWindow;
+import com.intellij.openapi.wm.ToolWindowManager;
+import com.intellij.pom.Navigatable;
+import com.intellij.ui.content.Content;
+import com.intellij.ui.content.ContentFactory;
+import com.intellij.util.ArrayUtil;
+import com.intellij.util.concurrency.SequentialTaskExecutor;
+import com.intellij.util.ui.MessageCategory;
+import com.intellij.util.ui.UIUtil;
+import java.util.ArrayList;
+import java.util.EnumSet;
+import java.util.List;
+import java.util.StringTokenizer;
+import java.util.UUID;
+import javax.swing.Icon;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+import org.jetbrains.ide.PooledThreadExecutor;
+
+/** @author Eugene Zhuravlev Date: 9/18/12 */
+public class BlazeProblemsViewConsole implements BlazeProblemsView {
+  private static final Logger LOG = Logger.getInstance(BlazeProblemsViewConsole.class);
+
+  private static final String BLAZE_PROBLEMS_TOOLWINDOW_ID = "Blaze Problems";
+  private static final EnumSet<ErrorTreeElementKind> ALL_MESSAGE_KINDS =
+      EnumSet.allOf(ErrorTreeElementKind.class);
+
+  private final ProblemsViewPanel myPanel;
+  private final SequentialTaskExecutor myViewUpdater =
+      new SequentialTaskExecutor(PooledThreadExecutor.INSTANCE);
+  private final Icon myActiveIcon = AllIcons.Toolwindows.Problems;
+  private final Icon myPassiveIcon = IconLoader.getDisabledIcon(myActiveIcon);
+
+  private final Project myProject;
+
+  public static BlazeProblemsViewConsole getImpl(Project project) {
+    BlazeProblemsViewConsole blazeProblemsViewConsole =
+        (BlazeProblemsViewConsole) ServiceManager.getService(project, BlazeProblemsView.class);
+    LOG.assertTrue(blazeProblemsViewConsole != null);
+    return blazeProblemsViewConsole;
+  }
+
+  public BlazeProblemsViewConsole(final Project project) {
+    myProject = project;
+    myPanel = new ProblemsViewPanel(project);
+    Disposer.register(project, () -> Disposer.dispose(myPanel));
+  }
+
+  public void createToolWindowContent(ToolWindow toolWindow) {
+    final Content content = ContentFactory.SERVICE.getInstance().createContent(myPanel, "", false);
+    toolWindow.getContentManager().addContent(content);
+    Disposer.register(myProject, () -> toolWindow.getContentManager().removeAllContents(true));
+    updateIcon();
+  }
+
+  @Override
+  public final void addMessage(IssueOutput issue, @NotNull UUID sessionId) {
+    final VirtualFile file =
+        issue.getFile() != null
+            ? VfsUtil.findFileByIoFile(issue.getFile(), true /* refresh */)
+            : null;
+    Navigatable navigatable = issue.getNavigatable();
+    if (navigatable == null && file != null) {
+      navigatable = new OpenFileDescriptor(myProject, file, -1, -1);
+    }
+    final IssueOutput.Category category = issue.getCategory();
+    final int type = translateCategory(category);
+    final String[] text = convertMessage(issue);
+    final String groupName = file != null ? file.getPresentableUrl() : category.name();
+    addMessage(
+        type,
+        text,
+        groupName,
+        navigatable,
+        getExportTextPrefix(issue),
+        getRenderTextPrefix(issue),
+        sessionId);
+  }
+
+  private static int translateCategory(IssueOutput.Category category) {
+    switch (category) {
+      case ERROR:
+        return MessageCategory.ERROR;
+      case WARNING:
+        return MessageCategory.WARNING;
+      case STATISTICS:
+        return MessageCategory.STATISTICS;
+      case INFORMATION:
+        return MessageCategory.INFORMATION;
+      default:
+        LOG.error("Unknown message category: " + category);
+        return 0;
+    }
+  }
+
+  private static String[] convertMessage(final IssueOutput issue) {
+    String text = issue.getMessage();
+    if (!text.contains("\n")) {
+      return new String[] {text};
+    }
+    final List<String> lines = new ArrayList<String>();
+    StringTokenizer tokenizer = new StringTokenizer(text, "\n", false);
+    while (tokenizer.hasMoreTokens()) {
+      lines.add(tokenizer.nextToken());
+    }
+    return ArrayUtil.toStringArray(lines);
+  }
+
+  private static String getExportTextPrefix(IssueOutput issue) {
+    int line = issue.getLine();
+    if (line >= 0) {
+      return String.format("line: %d", line);
+    }
+    return "";
+  }
+
+  private static String getRenderTextPrefix(IssueOutput issue) {
+    int line = issue.getLine();
+    if (line >= 0) {
+      return String.format("(%d, %d)", line, issue.getColumn());
+    }
+    return "";
+  }
+
+  @Override
+  public void clearOldMessages(@NotNull final UUID currentSessionId) {
+    myViewUpdater.execute(
+        new Runnable() {
+          @Override
+          public void run() {
+            cleanupChildrenRecursively(
+                myPanel.getErrorViewStructure().getRootElement(), currentSessionId);
+            updateIcon();
+            myPanel.reload();
+          }
+        });
+  }
+
+  private void cleanupChildrenRecursively(
+      @NotNull final Object fromElement, @NotNull UUID currentSessionId) {
+    final ErrorViewStructure structure = myPanel.getErrorViewStructure();
+    for (ErrorTreeElement element : structure.getChildElements(fromElement)) {
+      if (element instanceof GroupingElement) {
+        if (!currentSessionId.equals(element.getData())) {
+          structure.removeElement(element);
+        } else {
+          cleanupChildrenRecursively(element, currentSessionId);
+        }
+      } else {
+        if (!currentSessionId.equals(element.getData())) {
+          structure.removeElement(element);
+        }
+      }
+    }
+  }
+
+  public void addMessage(
+      final int type,
+      @NotNull final String[] text,
+      @Nullable final String groupName,
+      @Nullable final Navigatable navigatable,
+      @Nullable final String exportTextPrefix,
+      @Nullable final String rendererTextPrefix,
+      @Nullable final UUID sessionId) {
+
+    myViewUpdater.execute(
+        new Runnable() {
+          @Override
+          public void run() {
+            final ErrorViewStructure structure = myPanel.getErrorViewStructure();
+            final GroupingElement group = structure.lookupGroupingElement(groupName);
+            if (group != null && sessionId != null && !sessionId.equals(group.getData())) {
+              structure.removeElement(group);
+            }
+            if (navigatable != null) {
+              myPanel.addMessage(
+                  type,
+                  text,
+                  groupName,
+                  navigatable,
+                  exportTextPrefix,
+                  rendererTextPrefix,
+                  sessionId);
+            } else {
+              myPanel.addMessage(type, text, null, -1, -1, sessionId);
+            }
+            updateIcon();
+          }
+        });
+  }
+
+  private void updateIcon() {
+    UIUtil.invokeLaterIfNeeded(
+        new Runnable() {
+          @Override
+          public void run() {
+            if (!myProject.isDisposed()) {
+              final ToolWindow tw =
+                  ToolWindowManager.getInstance(myProject)
+                      .getToolWindow(BLAZE_PROBLEMS_TOOLWINDOW_ID);
+              if (tw != null) {
+                final boolean active =
+                    myPanel.getErrorViewStructure().hasMessages(ALL_MESSAGE_KINDS);
+                tw.setIcon(active ? myActiveIcon : myPassiveIcon);
+              }
+            }
+          }
+        });
+  }
+
+  public void setProgress(String text, float fraction) {
+    myPanel.setProgress(text, fraction);
+  }
+
+  public void setProgress(String text) {
+    myPanel.setProgressText(text);
+  }
+
+  public void clearProgress() {
+    myPanel.clearProgressData();
+  }
+}
diff --git a/clwb/src/com/google/idea/blaze/clwb/problemsview/BlazeProblemsViewConsoleToolWindowFactory.java b/clwb/src/com/google/idea/blaze/clwb/problemsview/BlazeProblemsViewConsoleToolWindowFactory.java
new file mode 100644
index 0000000..20ba562
--- /dev/null
+++ b/clwb/src/com/google/idea/blaze/clwb/problemsview/BlazeProblemsViewConsoleToolWindowFactory.java
@@ -0,0 +1,30 @@
+/*
+ * Copyright 2016 The Bazel Authors. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.idea.blaze.clwb.problemsview;
+
+import com.intellij.openapi.project.DumbAware;
+import com.intellij.openapi.project.Project;
+import com.intellij.openapi.wm.ToolWindow;
+import com.intellij.openapi.wm.ToolWindowFactory;
+
+/** Factory for console window. */
+public class BlazeProblemsViewConsoleToolWindowFactory implements DumbAware, ToolWindowFactory {
+  @Override
+  public void createToolWindowContent(Project project, ToolWindow toolWindow) {
+    BlazeProblemsViewConsole blazeProblemsViewConsole = BlazeProblemsViewConsole.getImpl(project);
+    blazeProblemsViewConsole.createToolWindowContent(toolWindow);
+  }
+}
diff --git a/clwb/src/com/google/idea/blaze/clwb/problemsview/ProblemsViewPanel.java b/clwb/src/com/google/idea/blaze/clwb/problemsview/ProblemsViewPanel.java
new file mode 100644
index 0000000..a16636b
--- /dev/null
+++ b/clwb/src/com/google/idea/blaze/clwb/problemsview/ProblemsViewPanel.java
@@ -0,0 +1,45 @@
+/*
+ * Copyright 2016 The Bazel Authors. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.idea.blaze.clwb.problemsview;
+
+import com.intellij.ide.errorTreeView.NewErrorTreeViewPanel;
+import com.intellij.openapi.actionSystem.DefaultActionGroup;
+import com.intellij.openapi.project.Project;
+
+class ProblemsViewPanel extends NewErrorTreeViewPanel {
+  ProblemsViewPanel(Project project) {
+    super(project, "blaze.problems.tool.window", false, true, null);
+    myTree.getEmptyText().setText("No problems found");
+  }
+
+  @Override
+  protected void fillRightToolbarGroup(DefaultActionGroup group) {
+    super.fillRightToolbarGroup(group);
+  }
+
+  @Override
+  protected void addExtraPopupMenuActions(DefaultActionGroup group) {}
+
+  @Override
+  protected boolean shouldShowFirstErrorInEditor() {
+    return false;
+  }
+
+  @Override
+  protected boolean canHideWarnings() {
+    return false;
+  }
+}
diff --git a/clwb/src/com/google/idea/blaze/clwb/run/BlazeCidrDebuggerSupportFactory.java b/clwb/src/com/google/idea/blaze/clwb/run/BlazeCidrDebuggerSupportFactory.java
new file mode 100644
index 0000000..2e7a019
--- /dev/null
+++ b/clwb/src/com/google/idea/blaze/clwb/run/BlazeCidrDebuggerSupportFactory.java
@@ -0,0 +1,38 @@
+/*
+ * Copyright 2016 The Bazel Authors. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.idea.blaze.clwb.run;
+
+import com.google.idea.blaze.base.run.BlazeCommandRunConfiguration;
+import com.intellij.execution.configurations.RunProfile;
+import com.intellij.xdebugger.evaluation.XDebuggerEditorsProvider;
+import com.jetbrains.cidr.execution.debugger.OCDebuggerLanguageSupportFactory;
+import javax.annotation.Nullable;
+
+/**
+ * A version of {@link OCDebuggerLanguageSupportFactory} which can accept {@link
+ * BlazeCommandRunConfiguration} when appropriate.
+ */
+public class BlazeCidrDebuggerSupportFactory extends OCDebuggerLanguageSupportFactory {
+  @Nullable
+  @Override
+  public XDebuggerEditorsProvider createEditor(RunProfile profile) {
+    if (profile instanceof BlazeCommandRunConfiguration
+        && RunConfigurationUtils.canUseClionRunner((BlazeCommandRunConfiguration) profile)) {
+      return createEditorProvider();
+    }
+    return null;
+  }
+}
diff --git a/clwb/src/com/google/idea/blaze/clwb/run/BlazeCidrLauncher.java b/clwb/src/com/google/idea/blaze/clwb/run/BlazeCidrLauncher.java
new file mode 100644
index 0000000..70f249e
--- /dev/null
+++ b/clwb/src/com/google/idea/blaze/clwb/run/BlazeCidrLauncher.java
@@ -0,0 +1,199 @@
+/*
+ * Copyright 2016 The Bazel Authors. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.idea.blaze.clwb.run;
+
+import com.google.common.collect.ImmutableList;
+import com.google.idea.blaze.base.async.process.LineProcessingOutputStream;
+import com.google.idea.blaze.base.command.BlazeCommand;
+import com.google.idea.blaze.base.command.BlazeCommandName;
+import com.google.idea.blaze.base.command.BlazeFlags;
+import com.google.idea.blaze.base.issueparser.IssueOutputLineProcessor;
+import com.google.idea.blaze.base.metrics.Action;
+import com.google.idea.blaze.base.metrics.LoggingService;
+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.BlazeCommandRunConfiguration;
+import com.google.idea.blaze.base.run.processhandler.LineProcessingProcessAdapter;
+import com.google.idea.blaze.base.run.processhandler.ScopedBlazeProcessHandler;
+import com.google.idea.blaze.base.run.state.BlazeCommandRunConfigurationCommonState;
+import com.google.idea.blaze.base.scope.BlazeContext;
+import com.google.idea.blaze.base.scope.scopes.IssuesScope;
+import com.google.idea.blaze.base.scope.scopes.LoggedTimingScope;
+import com.google.idea.blaze.base.settings.Blaze;
+import com.google.idea.blaze.base.settings.BlazeImportSettings;
+import com.google.idea.blaze.base.settings.BlazeImportSettingsManager;
+import com.intellij.execution.ExecutionException;
+import com.intellij.execution.configurations.CommandLineState;
+import com.intellij.execution.configurations.GeneralCommandLine;
+import com.intellij.execution.process.ProcessHandler;
+import com.intellij.execution.process.ProcessListener;
+import com.intellij.execution.runners.ExecutionEnvironment;
+import com.intellij.execution.ui.ConsoleView;
+import com.intellij.openapi.diagnostic.Logger;
+import com.intellij.openapi.project.Project;
+import com.intellij.xdebugger.XDebugSession;
+import com.jetbrains.cidr.cpp.execution.CLionRunParameters;
+import com.jetbrains.cidr.execution.CidrConsoleBuilder;
+import com.jetbrains.cidr.execution.TrivialInstaller;
+import com.jetbrains.cidr.execution.debugger.CidrDebugProcess;
+import com.jetbrains.cidr.execution.debugger.CidrLocalDebugProcess;
+import com.jetbrains.cidr.execution.testing.CidrLauncher;
+import com.jetbrains.cidr.execution.testing.OCGoogleTestConsoleProperties;
+import java.io.File;
+import java.util.Objects;
+import org.jetbrains.annotations.NotNull;
+
+/**
+ * Handles running/debugging cc_test and cc_binary targets in CLion. Sets up gdb when debugging, and
+ * uses the Google Test infrastructure for presenting test results.
+ */
+public final class BlazeCidrLauncher extends CidrLauncher {
+  private static final Logger LOG = Logger.getInstance(BlazeCidrLauncher.class);
+
+  private final Project project;
+  private final BlazeCommandRunConfiguration configuration;
+  private final BlazeCommandRunConfigurationCommonState handlerState;
+  private final BlazeCidrRunConfigurationRunner runner;
+  private final ExecutionEnvironment executionEnvironment;
+
+  public BlazeCidrLauncher(
+      BlazeCommandRunConfiguration configuration,
+      BlazeCidrRunConfigurationRunner runner,
+      ExecutionEnvironment environment) {
+    this.configuration = configuration;
+    this.handlerState =
+        (BlazeCommandRunConfigurationCommonState) configuration.getHandler().getState();
+    this.runner = runner;
+    this.executionEnvironment = environment;
+    this.project = configuration.getProject();
+  }
+
+  @Override
+  public ProcessHandler createProcess(CommandLineState state) throws ExecutionException {
+    BlazeImportSettings importSettings =
+        BlazeImportSettingsManager.getInstance(project).getImportSettings();
+    LOG.assertTrue(importSettings != null);
+
+    ProjectViewSet projectViewSet = ProjectViewManager.getInstance(project).getProjectViewSet();
+    LOG.assertTrue(projectViewSet != null);
+
+    state.setConsoleBuilder(createConsoleBuilder());
+
+    BlazeCommand blazeCommand =
+        BlazeCommand.builder(Blaze.getBuildSystem(project), handlerState.getCommand())
+            .addTargets(configuration.getTarget())
+            .addBlazeFlags(BlazeFlags.buildFlags(project, ProjectViewSet.builder().build()))
+            .addBlazeFlags(handlerState.getBlazeFlags())
+            .addExeFlags(handlerState.getExeFlags())
+            .build();
+
+    WorkspaceRoot workspaceRoot = WorkspaceRoot.fromImportSettings(importSettings);
+    return new ScopedBlazeProcessHandler(
+        project,
+        blazeCommand,
+        workspaceRoot,
+        new ScopedBlazeProcessHandler.ScopedProcessHandlerDelegate() {
+          @Override
+          public void onBlazeContextStart(BlazeContext context) {
+            context
+                .push(new LoggedTimingScope(project, Action.BLAZE_COMMAND_USAGE))
+                .push(new LoggedTimingScope(project, Action.BLAZE_CLION_TEST_RUN))
+                .push(new IssuesScope(project));
+          }
+
+          @Override
+          public ImmutableList<ProcessListener> createProcessListeners(BlazeContext context) {
+            LineProcessingOutputStream outputStream =
+                LineProcessingOutputStream.of(
+                    new IssueOutputLineProcessor(project, context, workspaceRoot));
+            return ImmutableList.of(new LineProcessingProcessAdapter(outputStream));
+          }
+        });
+  }
+
+  @Override
+  public CidrDebugProcess createDebugProcess(CommandLineState state, XDebugSession session)
+      throws ExecutionException {
+    TargetExpression target = configuration.getTarget();
+    if (target == null) {
+      return null;
+    }
+    if (runner.executableToDebug == null) {
+      return null;
+    }
+    ProjectViewSet projectViewSet = ProjectViewManager.getInstance(project).getProjectViewSet();
+    LOG.assertTrue(projectViewSet != null);
+    WorkspaceRoot workspaceRoot = WorkspaceRoot.fromProject(project);
+
+    GeneralCommandLine commandLine = new GeneralCommandLine(runner.executableToDebug.getPath());
+    File workingDir = workspaceRoot.directory();
+    commandLine.setWorkDirectory(workingDir);
+
+    TrivialInstaller installer = new TrivialInstaller(commandLine);
+    ImmutableList<String> startupCommands = getGdbStartupCommands(workingDir);
+    CLionRunParameters parameters =
+        new CLionRunParameters(
+            new BlazeGDBDriverConfiguration(project, startupCommands, workspaceRoot), installer);
+    CidrDebugProcess result =
+        new CidrLocalDebugProcess(parameters, session, state.getConsoleBuilder());
+
+    LoggingService.reportEvent(project, Action.BLAZE_CLION_TEST_DEBUG);
+    return result;
+  }
+
+  @NotNull
+  @Override
+  protected Project getProject() {
+    return project;
+  }
+
+  private CidrConsoleBuilder createConsoleBuilder() {
+    if (Objects.equals(handlerState.getCommand(), BlazeCommandName.TEST)) {
+      // Use the Google Test failure/success console instead of a standard console.
+      return new GoogleTestConsoleBuilder(configuration.getProject());
+    }
+    return new CidrConsoleBuilder(configuration.getProject());
+  }
+
+  private ImmutableList<String> getGdbStartupCommands(File workingDir) {
+    // Forge creates debug symbol paths rooted at /proc/self/cwd .
+    // We need to tell gdb to translate this path prefix to the user's workspace
+    // root so the IDE can find the files.
+    String from = "/proc/self/cwd";
+    String to = workingDir.getPath();
+    String subPathCommand = String.format("set substitute-path %s %s", from, to);
+
+    return ImmutableList.of(subPathCommand);
+  }
+
+  private final class GoogleTestConsoleBuilder extends CidrConsoleBuilder {
+    private GoogleTestConsoleBuilder(Project project) {
+      super(project);
+    }
+
+    @Override
+    protected ConsoleView createConsole() {
+      OCGoogleTestConsoleProperties consoleProperties =
+          new OCGoogleTestConsoleProperties(
+              configuration,
+              executionEnvironment.getExecutor(),
+              executionEnvironment.getExecutionTarget());
+      return createConsole(configuration.getType(), consoleProperties);
+    }
+  }
+}
diff --git a/clwb/src/com/google/idea/blaze/clwb/run/BlazeCidrRunConfigurationHandler.java b/clwb/src/com/google/idea/blaze/clwb/run/BlazeCidrRunConfigurationHandler.java
new file mode 100644
index 0000000..29aea18
--- /dev/null
+++ b/clwb/src/com/google/idea/blaze/clwb/run/BlazeCidrRunConfigurationHandler.java
@@ -0,0 +1,93 @@
+/*
+ * Copyright 2016 The Bazel Authors. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.idea.blaze.clwb.run;
+
+import com.google.idea.blaze.base.command.BlazeCommandName;
+import com.google.idea.blaze.base.run.BlazeCommandRunConfiguration;
+import com.google.idea.blaze.base.run.BlazeConfigurationNameBuilder;
+import com.google.idea.blaze.base.run.confighandler.BlazeCommandGenericRunConfigurationRunner;
+import com.google.idea.blaze.base.run.confighandler.BlazeCommandRunConfigurationHandler;
+import com.google.idea.blaze.base.run.confighandler.BlazeCommandRunConfigurationRunner;
+import com.google.idea.blaze.base.run.state.BlazeCommandRunConfigurationCommonState;
+import com.google.idea.blaze.base.settings.Blaze;
+import com.intellij.execution.Executor;
+import com.intellij.execution.RunnerAndConfigurationSettings;
+import com.intellij.execution.configurations.RunConfiguration;
+import com.intellij.execution.configurations.RuntimeConfigurationException;
+import com.intellij.execution.runners.ExecutionEnvironment;
+import javax.annotation.Nullable;
+import javax.swing.Icon;
+
+/** CLion-specific handler for {@link BlazeCommandRunConfiguration}s. */
+public final class BlazeCidrRunConfigurationHandler implements BlazeCommandRunConfigurationHandler {
+
+  private final String buildSystemName;
+  private final BlazeCommandRunConfigurationCommonState state;
+
+  public BlazeCidrRunConfigurationHandler(BlazeCommandRunConfiguration configuration) {
+    this.buildSystemName = Blaze.buildSystemName(configuration.getProject());
+    this.state = new BlazeCommandRunConfigurationCommonState(buildSystemName);
+  }
+
+  @Override
+  public BlazeCommandRunConfigurationCommonState getState() {
+    return state;
+  }
+
+  @Override
+  public BlazeCommandRunConfigurationRunner createRunner(
+      Executor executor, ExecutionEnvironment environment) {
+    RunnerAndConfigurationSettings settings = environment.getRunnerAndConfigurationSettings();
+    RunConfiguration config = settings != null ? settings.getConfiguration() : null;
+    if (config instanceof BlazeCommandRunConfiguration
+        && RunConfigurationUtils.canUseClionRunner((BlazeCommandRunConfiguration) config)) {
+      return new BlazeCidrRunConfigurationRunner((BlazeCommandRunConfiguration) config);
+    }
+    return new BlazeCommandGenericRunConfigurationRunner();
+  }
+
+  @Override
+  public void checkConfiguration() throws RuntimeConfigurationException {
+    state.validate(buildSystemName);
+  }
+
+  @Override
+  @Nullable
+  public String suggestedName(BlazeCommandRunConfiguration configuration) {
+    if (configuration.getTarget() == null) {
+      return null;
+    }
+    return new BlazeConfigurationNameBuilder(configuration).build();
+  }
+
+  @Override
+  @Nullable
+  public String getCommandName() {
+    BlazeCommandName command = state.getCommand();
+    return command != null ? command.toString() : null;
+  }
+
+  @Override
+  public String getHandlerName() {
+    return "CLion Handler";
+  }
+
+  @Override
+  @Nullable
+  public Icon getExecutorIcon(RunConfiguration configuration, Executor executor) {
+    return null;
+  }
+}
diff --git a/clwb/src/com/google/idea/blaze/clwb/run/BlazeCidrRunConfigurationHandlerProvider.java b/clwb/src/com/google/idea/blaze/clwb/run/BlazeCidrRunConfigurationHandlerProvider.java
new file mode 100644
index 0000000..6d78d29
--- /dev/null
+++ b/clwb/src/com/google/idea/blaze/clwb/run/BlazeCidrRunConfigurationHandlerProvider.java
@@ -0,0 +1,41 @@
+/*
+ * Copyright 2016 The Bazel Authors. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.idea.blaze.clwb.run;
+
+import com.google.idea.blaze.base.model.primitives.Kind;
+import com.google.idea.blaze.base.run.BlazeCommandRunConfiguration;
+import com.google.idea.blaze.base.run.confighandler.BlazeCommandRunConfigurationHandler;
+import com.google.idea.blaze.base.run.confighandler.BlazeCommandRunConfigurationHandlerProvider;
+
+/** CLion-specific handler for {@link BlazeCommandRunConfiguration}s. */
+public class BlazeCidrRunConfigurationHandlerProvider
+    implements BlazeCommandRunConfigurationHandlerProvider {
+
+  @Override
+  public boolean canHandleKind(Kind kind) {
+    return RunConfigurationUtils.canUseClionHandler(kind);
+  }
+
+  @Override
+  public BlazeCommandRunConfigurationHandler createHandler(BlazeCommandRunConfiguration config) {
+    return new BlazeCidrRunConfigurationHandler(config);
+  }
+
+  @Override
+  public String getId() {
+    return "BlazeCLionRunConfigurationHandlerProvider";
+  }
+}
diff --git a/clwb/src/com/google/idea/blaze/clwb/run/BlazeCidrRunConfigurationRunner.java b/clwb/src/com/google/idea/blaze/clwb/run/BlazeCidrRunConfigurationRunner.java
new file mode 100644
index 0000000..515e4bb
--- /dev/null
+++ b/clwb/src/com/google/idea/blaze/clwb/run/BlazeCidrRunConfigurationRunner.java
@@ -0,0 +1,175 @@
+/*
+ * Copyright 2016 The Bazel Authors. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.idea.blaze.clwb.run;
+
+import com.google.common.collect.Iterables;
+import com.google.common.collect.Lists;
+import com.google.common.util.concurrent.ListenableFuture;
+import com.google.idea.blaze.base.async.executor.BlazeExecutor;
+import com.google.idea.blaze.base.async.process.ExternalTask;
+import com.google.idea.blaze.base.async.process.LineProcessingOutputStream;
+import com.google.idea.blaze.base.command.BlazeCommand;
+import com.google.idea.blaze.base.command.BlazeCommandName;
+import com.google.idea.blaze.base.command.BlazeFlags;
+import com.google.idea.blaze.base.command.ExperimentalShowArtifactsLineProcessor;
+import com.google.idea.blaze.base.issueparser.IssueOutputLineProcessor;
+import com.google.idea.blaze.base.metrics.Action;
+import com.google.idea.blaze.base.model.primitives.WorkspaceRoot;
+import com.google.idea.blaze.base.projectview.ProjectViewManager;
+import com.google.idea.blaze.base.projectview.ProjectViewSet;
+import com.google.idea.blaze.base.run.BlazeCommandRunConfiguration;
+import com.google.idea.blaze.base.run.confighandler.BlazeCommandRunConfigurationRunner;
+import com.google.idea.blaze.base.run.state.BlazeCommandRunConfigurationCommonState;
+import com.google.idea.blaze.base.scope.BlazeContext;
+import com.google.idea.blaze.base.scope.ScopedTask;
+import com.google.idea.blaze.base.scope.output.StatusOutput;
+import com.google.idea.blaze.base.scope.scopes.BlazeConsoleScope;
+import com.google.idea.blaze.base.scope.scopes.IssuesScope;
+import com.google.idea.blaze.base.scope.scopes.LoggedTimingScope;
+import com.google.idea.blaze.base.settings.Blaze;
+import com.google.idea.blaze.base.util.SaveUtil;
+import com.google.idea.common.experiments.BoolExperiment;
+import com.intellij.execution.ExecutionException;
+import com.intellij.execution.Executor;
+import com.intellij.execution.configurations.RunProfileState;
+import com.intellij.execution.executors.DefaultDebugExecutor;
+import com.intellij.execution.runners.ExecutionEnvironment;
+import com.intellij.openapi.diagnostic.Logger;
+import com.intellij.openapi.project.Project;
+import com.intellij.openapi.vfs.LocalFileSystem;
+import com.jetbrains.cidr.execution.CidrCommandLineState;
+import java.io.File;
+import java.util.List;
+
+/** CLion-specific handler for {@link BlazeCommandRunConfiguration}s. */
+public class BlazeCidrRunConfigurationRunner implements BlazeCommandRunConfigurationRunner {
+
+  private static final Logger LOG = Logger.getInstance(ExternalTask.class);
+
+  private static final BoolExperiment FORCE_DEBUG_BUILD_FOR_DEBUGGING_TEST =
+      new BoolExperiment("clwb.force.debug.build.for.debugging.test", true);
+
+  private final BlazeCommandRunConfiguration configuration;
+
+  /** Calculated during the before-run task, and made available to the debugger. */
+  File executableToDebug = null;
+
+  BlazeCidrRunConfigurationRunner(BlazeCommandRunConfiguration configuration) {
+    this.configuration = configuration;
+  }
+
+  @Override
+  public RunProfileState getRunProfileState(Executor executor, ExecutionEnvironment env) {
+    return new CidrCommandLineState(env, new BlazeCidrLauncher(configuration, this, env));
+  }
+
+  @Override
+  public boolean executeBeforeRunTask(ExecutionEnvironment environment) {
+    if (!isDebugging(environment)) {
+      return true;
+    }
+    try {
+      File executable = getExecutableToDebug();
+      if (executable != null) {
+        executableToDebug = executable;
+        return true;
+      }
+    } catch (ExecutionException e) {
+      LOG.error(e.getMessage());
+    }
+    return false;
+  }
+
+  private static boolean isDebugging(ExecutionEnvironment environment) {
+    Executor executor = environment.getExecutor();
+    return executor instanceof DefaultDebugExecutor;
+  }
+
+  /**
+   * Builds blaze C/C++ target in debug mode, and returns the output build artifact.
+   *
+   * @throws ExecutionException if no unique output artifact was found.
+   */
+  private File getExecutableToDebug() throws ExecutionException {
+    final Project project = configuration.getProject();
+    final BlazeCommandRunConfigurationCommonState handlerState =
+        (BlazeCommandRunConfigurationCommonState) configuration.getHandler().getState();
+    final WorkspaceRoot workspaceRoot = WorkspaceRoot.fromProject(project);
+    final ProjectViewSet projectViewSet =
+        ProjectViewManager.getInstance(project).getProjectViewSet();
+
+    final List<File> outputArtifacts = Lists.newArrayList();
+    final ListenableFuture<Void> buildOperation =
+        BlazeExecutor.submitTask(
+            project,
+            new ScopedTask() {
+              @Override
+              protected void execute(BlazeContext context) {
+                context
+                    .push(new LoggedTimingScope(project, Action.BLAZE_COMMAND_USAGE))
+                    .push(new IssuesScope(project))
+                    .push(new BlazeConsoleScope.Builder(project).build());
+                ;
+
+                context.output(new StatusOutput("Building debug binary"));
+
+                BlazeCommand.Builder command =
+                    BlazeCommand.builder(Blaze.getBuildSystem(project), BlazeCommandName.BUILD)
+                        .addTargets(configuration.getTarget())
+                        .addBlazeFlags(BlazeFlags.buildFlags(project, projectViewSet))
+                        .addBlazeFlags(handlerState.getBlazeFlags())
+                        .addExeFlags(handlerState.getExeFlags());
+
+                command.addBlazeFlags("--experimental_show_artifacts");
+
+                // If we are trying to debug, make sure we are building in debug mode.
+                // This can cause a rebuild, so it is a heavyweight setting.
+                if (FORCE_DEBUG_BUILD_FOR_DEBUGGING_TEST.getValue()) {
+                  command.addBlazeFlags("-c", "dbg");
+                }
+
+                ExternalTask.builder(workspaceRoot)
+                    .addBlazeCommand(command.build())
+                    .context(context)
+                    .stderr(
+                        LineProcessingOutputStream.of(
+                            new ExperimentalShowArtifactsLineProcessor(outputArtifacts, ""),
+                            new IssueOutputLineProcessor(project, context, workspaceRoot)))
+                    .build()
+                    .run();
+              }
+            });
+
+    try {
+      SaveUtil.saveAllFiles();
+      buildOperation.get();
+    } catch (InterruptedException | java.util.concurrent.ExecutionException e) {
+      throw new ExecutionException(e);
+    }
+    if (outputArtifacts.isEmpty()) {
+      throw new ExecutionException(
+          String.format("No output artifacts found when building %s", configuration.getTarget()));
+    }
+    if (outputArtifacts.size() > 1) {
+      throw new ExecutionException(
+          String.format(
+              "More than 1 executable was produced when building %s; don't know which one to debug",
+              configuration.getTarget()));
+    }
+    LocalFileSystem.getInstance().refreshIoFiles(outputArtifacts);
+    return Iterables.getOnlyElement(outputArtifacts);
+  }
+}
diff --git a/clwb/src/com/google/idea/blaze/clwb/run/BlazeCommandFlags.java b/clwb/src/com/google/idea/blaze/clwb/run/BlazeCommandFlags.java
new file mode 100644
index 0000000..12020f2
--- /dev/null
+++ b/clwb/src/com/google/idea/blaze/clwb/run/BlazeCommandFlags.java
@@ -0,0 +1,132 @@
+/*
+ * Copyright 2016 The Bazel Authors. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.idea.blaze.clwb.run;
+
+import com.google.common.base.Objects;
+import com.google.common.base.Strings;
+import com.google.common.collect.ImmutableList;
+import com.google.idea.blaze.base.ui.UiUtil;
+import com.intellij.openapi.util.InvalidDataException;
+import com.intellij.openapi.util.JDOMExternalizable;
+import com.intellij.openapi.util.WriteExternalException;
+import com.intellij.util.execution.ParametersListUtil;
+import java.util.List;
+import javax.swing.JComponent;
+import javax.swing.JLabel;
+import javax.swing.JTextArea;
+import org.jdom.Element;
+
+final class BlazeCommandFlags implements JDOMExternalizable {
+  public static final class Editor {
+    private final JTextArea blazeFlagsField = new JTextArea(5, 0);
+    private final JTextArea exeFlagsField = new JTextArea(5, 0);
+
+    public JComponent getEditorComponent() {
+      return UiUtil.createBox(
+          new JLabel("Blaze flags:"),
+          blazeFlagsField,
+          new JLabel("Executable flags:"),
+          exeFlagsField);
+    }
+
+    public void setText(BlazeCommandFlags blazeCommandFlags) {
+      blazeFlagsField.setText(ParametersListUtil.join(blazeCommandFlags.getBlazeFlags()));
+      exeFlagsField.setText(ParametersListUtil.join(blazeCommandFlags.getExeFlags()));
+    }
+
+    public BlazeCommandFlags getBlazeCommandFlags() {
+      ImmutableList<String> blazeFlags =
+          ImmutableList.copyOf(
+              ParametersListUtil.parse(Strings.nullToEmpty(blazeFlagsField.getText())));
+      ImmutableList<String> exeFlags =
+          ImmutableList.copyOf(
+              ParametersListUtil.parse(Strings.nullToEmpty(exeFlagsField.getText())));
+      return new BlazeCommandFlags(blazeFlags, exeFlags);
+    }
+  }
+
+  private static final String USER_BLAZE_FLAG_TAG = "blaze-user-flag";
+  private static final String USER_EXE_FLAG_TAG = "blaze-user-exe-flag";
+
+  private ImmutableList<String> blazeFlags = ImmutableList.of();
+  private ImmutableList<String> exeFlags = ImmutableList.of();
+
+  public BlazeCommandFlags() {
+    this.blazeFlags = ImmutableList.of();
+    this.exeFlags = ImmutableList.of();
+  }
+
+  public BlazeCommandFlags(ImmutableList<String> blazeFlags, ImmutableList<String> exeFlags) {
+    this.blazeFlags = blazeFlags;
+    this.exeFlags = exeFlags;
+  }
+
+  public ImmutableList<String> getBlazeFlags() {
+    return blazeFlags;
+  }
+
+  public ImmutableList<String> getExeFlags() {
+    return exeFlags;
+  }
+
+  @Override
+  public void readExternal(Element element) throws InvalidDataException {
+    blazeFlags = loadUserFlags(element, USER_BLAZE_FLAG_TAG);
+    exeFlags = loadUserFlags(element, USER_EXE_FLAG_TAG);
+  }
+
+  private static ImmutableList<String> loadUserFlags(Element root, String tag) {
+    ImmutableList.Builder<String> flagsBuilder = ImmutableList.builder();
+    for (Element e : root.getChildren(tag)) {
+      String flag = e.getTextTrim();
+      if (flag != null && !flag.isEmpty()) {
+        flagsBuilder.add(flag);
+      }
+    }
+    return flagsBuilder.build();
+  }
+
+  @Override
+  public void writeExternal(Element element) throws WriteExternalException {
+    saveUserFlags(element, blazeFlags, USER_BLAZE_FLAG_TAG);
+    saveUserFlags(element, exeFlags, USER_EXE_FLAG_TAG);
+  }
+
+  private static void saveUserFlags(Element root, List<String> flags, String tag) {
+    for (String flag : flags) {
+      Element child = new Element(tag);
+      child.setText(flag);
+      root.addContent(child);
+    }
+  }
+
+  @Override
+  public boolean equals(Object o) {
+    if (this == o) {
+      return true;
+    }
+    if (o == null || getClass() != o.getClass()) {
+      return false;
+    }
+    BlazeCommandFlags that = (BlazeCommandFlags) o;
+    return Objects.equal(blazeFlags, that.blazeFlags) && Objects.equal(exeFlags, that.exeFlags);
+  }
+
+  @Override
+  public int hashCode() {
+    return Objects.hashCode(blazeFlags, exeFlags);
+  }
+}
diff --git a/clwb/src/com/google/idea/blaze/clwb/run/BlazeCppRunner.java b/clwb/src/com/google/idea/blaze/clwb/run/BlazeCppRunner.java
new file mode 100644
index 0000000..506cbd5
--- /dev/null
+++ b/clwb/src/com/google/idea/blaze/clwb/run/BlazeCppRunner.java
@@ -0,0 +1,37 @@
+/*
+ * Copyright 2016 The Bazel Authors. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.idea.blaze.clwb.run;
+
+import com.google.idea.blaze.base.run.BlazeCommandRunConfiguration;
+import com.intellij.execution.configurations.RunProfile;
+import com.jetbrains.cidr.cpp.execution.CLionRunner;
+
+/**
+ * A version of CPPRunner which can accept {@link BlazeCommandRunConfiguration} when appropriate.
+ */
+public class BlazeCppRunner extends CLionRunner {
+
+  @Override
+  public String getRunnerId() {
+    return "BlazeCppAppRunner";
+  }
+
+  @Override
+  public boolean canRun(String executorId, RunProfile profile) {
+    return profile instanceof BlazeCommandRunConfiguration
+        && RunConfigurationUtils.canUseClionRunner((BlazeCommandRunConfiguration) profile);
+  }
+}
diff --git a/clwb/src/com/google/idea/blaze/clwb/run/BlazeGDBDriverConfiguration.java b/clwb/src/com/google/idea/blaze/clwb/run/BlazeGDBDriverConfiguration.java
new file mode 100644
index 0000000..82c6a5f
--- /dev/null
+++ b/clwb/src/com/google/idea/blaze/clwb/run/BlazeGDBDriverConfiguration.java
@@ -0,0 +1,102 @@
+/*
+ * Copyright 2016 The Bazel Authors. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.idea.blaze.clwb.run;
+
+import com.google.common.collect.ImmutableList;
+import com.google.idea.blaze.base.model.BlazeProjectData;
+import com.google.idea.blaze.base.model.primitives.WorkspaceRoot;
+import com.google.idea.blaze.base.sync.data.BlazeProjectDataManager;
+import com.intellij.execution.ExecutionException;
+import com.intellij.execution.configurations.GeneralCommandLine;
+import com.intellij.openapi.diagnostic.Logger;
+import com.intellij.openapi.project.Project;
+import com.intellij.openapi.util.io.FileUtil;
+import com.jetbrains.cidr.cpp.execution.debugger.backend.GDBDriverConfiguration;
+import com.jetbrains.cidr.execution.Installer;
+import com.jetbrains.cidr.execution.debugger.backend.DebuggerDriver;
+import java.io.File;
+import java.io.IOException;
+import javax.annotation.Nullable;
+
+final class BlazeGDBDriverConfiguration extends GDBDriverConfiguration {
+  private static final Logger LOG = Logger.getInstance(BlazeGDBDriverConfiguration.class);
+
+  private final ImmutableList<String> startupCommands;
+  private final WorkspaceRoot workspaceRoot;
+  private final Project project;
+
+  BlazeGDBDriverConfiguration(
+      Project project, ImmutableList<String> startupCommands, WorkspaceRoot workspaceRoot) {
+    this.project = project;
+    this.startupCommands = startupCommands;
+    this.workspaceRoot = workspaceRoot;
+  }
+
+  @Override
+  public GeneralCommandLine createDriverCommandLine(DebuggerDriver driver, Installer installer)
+      throws ExecutionException {
+    GeneralCommandLine driverCommandLine = super.createDriverCommandLine(driver, installer);
+
+    // Add our GDB commands to run after startup
+    for (String command : startupCommands) {
+      driverCommandLine.addParameter("-ex");
+      driverCommandLine.addParameter(command);
+    }
+    return driverCommandLine;
+  }
+
+  @Override
+  public String convertToLocalPath(@Nullable String absolutePath) {
+    if (absolutePath != null) {
+      final File file = new File(absolutePath);
+      final File workspaceDirectory = workspaceRoot.directory();
+      final String relativePath = gdbPathToWorkspaceRelativePath(workspaceDirectory, file);
+      File git5SafeFile = null;
+      BlazeProjectData blazeProjectData =
+          BlazeProjectDataManager.getInstance(project).getBlazeProjectData();
+      if (blazeProjectData != null) {
+        git5SafeFile = blazeProjectData.workspacePathResolver.resolveToFile(relativePath);
+      }
+      absolutePath = git5SafeFile == null ? null : git5SafeFile.getPath();
+    }
+    return super.convertToLocalPath(absolutePath);
+  }
+
+  /**
+   * Heuristic to try to handle the case where the file returned by gdb uses the canonical path but
+   * the user imported their project using a non-canonical path. For example, this handles the case
+   * where the user keeps their git5 repos on a different mount and accesses them via a symlink from
+   * their home directory.
+   *
+   * @param workspaceDirectory workspace root, as it was imported into CLion
+   * @param file file returned by GDB
+   * @return The relative path for {@param file} as it was imported into CLion
+   */
+  private String gdbPathToWorkspaceRelativePath(File workspaceDirectory, File file) {
+    try {
+      File canonicalWorkspaceDirectory = workspaceDirectory.getCanonicalFile();
+      File canonicalFile = file.getCanonicalFile();
+      String relativeCanonicalPath =
+          FileUtil.getRelativePath(canonicalWorkspaceDirectory, canonicalFile);
+      if (relativeCanonicalPath != null) {
+        return relativeCanonicalPath;
+      }
+    } catch (IOException e) {
+      LOG.info(e);
+    }
+    return file.getPath();
+  }
+}
diff --git a/clwb/src/com/google/idea/blaze/clwb/run/RunConfigurationUtils.java b/clwb/src/com/google/idea/blaze/clwb/run/RunConfigurationUtils.java
new file mode 100644
index 0000000..a6c96ad
--- /dev/null
+++ b/clwb/src/com/google/idea/blaze/clwb/run/RunConfigurationUtils.java
@@ -0,0 +1,43 @@
+/*
+ * Copyright 2016 The Bazel Authors. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.idea.blaze.clwb.run;
+
+import com.google.idea.blaze.base.command.BlazeCommandName;
+import com.google.idea.blaze.base.model.primitives.Kind;
+import com.google.idea.blaze.base.run.BlazeCommandRunConfiguration;
+import com.google.idea.blaze.base.run.state.BlazeCommandRunConfigurationCommonState;
+
+/** Utility methods for CLion run configurations */
+public class RunConfigurationUtils {
+
+  static boolean canUseClionHandler(Kind kind) {
+    return kind == Kind.CC_TEST || kind == Kind.CC_BINARY;
+  }
+
+  static boolean canUseClionRunner(BlazeCommandRunConfiguration config) {
+    Kind kind = config.getKindForTarget();
+    BlazeCommandRunConfigurationCommonState handlerState =
+        config.getHandlerStateIfType(BlazeCommandRunConfigurationCommonState.class);
+    if (handlerState == null) {
+      return false;
+    }
+    BlazeCommandName command = handlerState.getCommand();
+    return kind != null
+        && command != null
+        && ((kind == Kind.CC_TEST && command.equals(BlazeCommandName.TEST))
+            || (kind == Kind.CC_BINARY && command.equals(BlazeCommandName.BUILD)));
+  }
+}
diff --git a/clwb/src/com/google/idea/blaze/clwb/run/producers/BlazeCidrTestConfigurationProducer.java b/clwb/src/com/google/idea/blaze/clwb/run/producers/BlazeCidrTestConfigurationProducer.java
new file mode 100644
index 0000000..3132f87
--- /dev/null
+++ b/clwb/src/com/google/idea/blaze/clwb/run/producers/BlazeCidrTestConfigurationProducer.java
@@ -0,0 +1,273 @@
+/*
+ * Copyright 2016 The Bazel Authors. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.idea.blaze.clwb.run.producers;
+
+import com.google.common.collect.ImmutableList;
+import com.google.idea.blaze.base.command.BlazeCommandName;
+import com.google.idea.blaze.base.command.BlazeFlags;
+import com.google.idea.blaze.base.ideinfo.RuleIdeInfo;
+import com.google.idea.blaze.base.model.primitives.Label;
+import com.google.idea.blaze.base.run.BlazeCommandRunConfiguration;
+import com.google.idea.blaze.base.run.BlazeCommandRunConfigurationType;
+import com.google.idea.blaze.base.run.TestRuleFinder;
+import com.google.idea.blaze.base.run.TestRuleHeuristic;
+import com.google.idea.blaze.base.run.producers.BlazeRunConfigurationProducer;
+import com.google.idea.blaze.base.run.state.BlazeCommandRunConfigurationCommonState;
+import com.google.idea.blaze.base.settings.Blaze;
+import com.intellij.execution.Location;
+import com.intellij.execution.actions.ConfigurationContext;
+import com.intellij.openapi.util.Couple;
+import com.intellij.openapi.util.Ref;
+import com.intellij.openapi.vfs.VirtualFile;
+import com.intellij.psi.PsiElement;
+import com.intellij.psi.PsiFile;
+import com.intellij.psi.util.PsiTreeUtil;
+import com.jetbrains.cidr.execution.testing.CidrTestUtil;
+import com.jetbrains.cidr.lang.psi.OCFile;
+import com.jetbrains.cidr.lang.psi.OCFunctionDefinition;
+import com.jetbrains.cidr.lang.psi.OCMacroCall;
+import com.jetbrains.cidr.lang.psi.OCMacroCallArgument;
+import com.jetbrains.cidr.lang.psi.OCStruct;
+import com.jetbrains.cidr.lang.symbols.OCSymbol;
+import com.jetbrains.cidr.lang.symbols.cpp.OCFunctionSymbol;
+import com.jetbrains.cidr.lang.symbols.cpp.OCStructSymbol;
+import com.jetbrains.cidr.lang.symbols.cpp.OCSymbolWithQualifiedName;
+import java.io.File;
+import java.util.Collection;
+import java.util.List;
+import java.util.Objects;
+import javax.annotation.Nullable;
+
+/** Producer for run configurations related to C/C++ test classes in Blaze. */
+public class BlazeCidrTestConfigurationProducer
+    extends BlazeRunConfigurationProducer<BlazeCommandRunConfiguration> {
+
+  private static class TestTarget {
+    @Nullable
+    private static TestTarget createFromFile(@Nullable PsiElement element) {
+      return createFromClassAndMethod(element, null, null);
+    }
+
+    @Nullable
+    private static TestTarget createFromClass(@Nullable PsiElement element, String className) {
+      return createFromClassAndMethod(element, className, null);
+    }
+
+    @Nullable
+    private static TestTarget createFromClassAndMethod(
+        @Nullable PsiElement element, String classOrSuiteName, @Nullable String testName) {
+      Label label = getCcTestTarget(element);
+      if (label == null) {
+        return null;
+      }
+      String filter = null;
+      if (classOrSuiteName != null) {
+        filter = classOrSuiteName;
+        if (testName != null) {
+          filter += "." + testName;
+        }
+      }
+      return new TestTarget(element, label, filter);
+    }
+
+    private final PsiElement element;
+    private final Label label;
+    @Nullable private final String testFilterArg;
+    private final String name;
+
+    private TestTarget(PsiElement element, Label label, @Nullable String testFilter) {
+      this.element = element;
+      this.label = label;
+      if (testFilter != null) {
+        testFilterArg = BlazeFlags.TEST_FILTER + "=" + testFilter;
+        name = String.format("%s (%s)", testFilter, label.toString());
+      } else {
+        testFilterArg = null;
+        name = label.toString();
+      }
+    }
+  }
+
+  public BlazeCidrTestConfigurationProducer() {
+    super(BlazeCommandRunConfigurationType.getInstance());
+  }
+
+  @Override
+  protected boolean doSetupConfigFromContext(
+      BlazeCommandRunConfiguration configuration,
+      ConfigurationContext context,
+      Ref<PsiElement> sourceElement) {
+
+    TestTarget testObject = findTestObject(context.getLocation());
+    if (testObject == null) {
+      return false;
+    }
+    sourceElement.set(testObject.element);
+    configuration.setTarget(testObject.label);
+    BlazeCommandRunConfigurationCommonState handlerState =
+        configuration.getHandlerStateIfType(BlazeCommandRunConfigurationCommonState.class);
+    if (handlerState == null) {
+      return false;
+    }
+    handlerState.setCommand(BlazeCommandName.TEST);
+
+    ImmutableList.Builder<String> flags = ImmutableList.builder();
+    if (testObject.testFilterArg != null) {
+      flags.add(testObject.testFilterArg);
+    }
+    flags.add(BlazeFlags.TEST_OUTPUT_STREAMED);
+    flags.addAll(handlerState.getBlazeFlags());
+
+    handlerState.setBlazeFlags(flags.build());
+    configuration.setName(
+        String.format(
+            "%s test: %s", Blaze.buildSystemName(configuration.getProject()), testObject.name));
+    return true;
+  }
+
+  @Override
+  protected boolean doIsConfigFromContext(
+      BlazeCommandRunConfiguration configuration, ConfigurationContext context) {
+    BlazeCommandRunConfigurationCommonState handlerState =
+        configuration.getHandlerStateIfType(BlazeCommandRunConfigurationCommonState.class);
+    if (handlerState == null) {
+      return false;
+    }
+    if (!Objects.equals(handlerState.getCommand(), BlazeCommandName.TEST)) {
+      return false;
+    }
+    TestTarget testObject = findTestObject(context.getLocation());
+    if (testObject == null) {
+      return false;
+    }
+    List<String> flags = handlerState.getBlazeFlags();
+    return testObject.label.equals(configuration.getTarget())
+        && (testObject.testFilterArg == null || flags.contains(testObject.testFilterArg));
+  }
+
+  @Nullable
+  private static TestTarget findTestObject(Location<?> location) {
+    // Copied from on CidrGoogleTestRunConfigurationProducer::findTestObject.
+
+    // Precedence order (decreasing): class/function, macro, file
+    PsiElement element = location.getPsiElement();
+    PsiElement parent =
+        PsiTreeUtil.getNonStrictParentOfType(element, OCFunctionDefinition.class, OCStruct.class);
+
+    OCStructSymbol parentSymbol;
+    if (parent instanceof OCStruct
+        && ((parentSymbol = ((OCStruct) parent).getSymbol()) != null)
+        && CidrTestUtil.isGoogleTestClass(parentSymbol)) {
+      Couple<String> name = CidrTestUtil.extractGoogleTestName(parentSymbol);
+      if (name != null) {
+        return TestTarget.createFromClassAndMethod(parent, name.first, name.second);
+      }
+      String className = parentSymbol.getQualifiedName().getName();
+      return TestTarget.createFromClass(parent, className);
+    } else if (parent instanceof OCFunctionDefinition) {
+      OCFunctionSymbol symbol = ((OCFunctionDefinition) parent).getSymbol();
+      if (symbol != null) {
+        OCSymbolWithQualifiedName<?> resolvedOwner = symbol.getResolvedOwner();
+        if (resolvedOwner != null) {
+          OCSymbol<?> owner = resolvedOwner.getDefinitionSymbol();
+          if (owner instanceof OCStructSymbol
+              && CidrTestUtil.isGoogleTestClass((OCStructSymbol) owner)) {
+            OCStruct struct = (OCStruct) owner.locateDefinition();
+            Couple<String> name = CidrTestUtil.extractGoogleTestName((OCStructSymbol) owner);
+            if (name != null) {
+              return TestTarget.createFromClassAndMethod(struct, name.first, name.second);
+            }
+            return TestTarget.createFromClass(
+                struct, ((OCStructSymbol) owner).getQualifiedName().getName());
+          }
+        }
+      }
+    }
+
+    // if we're still here, let's test for a macro and, as a last resort, a file.
+    parent = PsiTreeUtil.getNonStrictParentOfType(element, OCMacroCall.class, OCFile.class);
+    if (parent instanceof OCMacroCall) {
+      OCMacroCall gtestMacro = CidrTestUtil.findGoogleTestMacros(parent);
+      if (gtestMacro != null) {
+        List<OCMacroCallArgument> arguments = gtestMacro.getArguments();
+        if (arguments.size() >= 2) {
+          OCMacroCallArgument suiteArg = arguments.get(0);
+          OCMacroCallArgument testArg = arguments.get(1);
+
+          // if the element is the first argument of macro call,
+          // then running entire suite, otherwise only a current test
+          boolean isSuite =
+              isFirstArgument(PsiTreeUtil.getParentOfType(element, OCMacroCallArgument.class))
+                  || isFirstArgument(element.getPrevSibling());
+          String suiteName = CidrTestUtil.extractArgumentValue(suiteArg);
+          String testName = CidrTestUtil.extractArgumentValue(testArg);
+          OCStructSymbol symbol =
+              CidrTestUtil.findGoogleTestSymbol(element.getProject(), suiteName, testName);
+          if (symbol != null) {
+            OCStruct targetElement = (OCStruct) symbol.locateDefinition();
+            return TestTarget.createFromClassAndMethod(
+                targetElement, suiteName, isSuite ? null : testName);
+          }
+        }
+      }
+      Couple<String> suite = CidrTestUtil.extractFullSuiteNameFromMacro(parent);
+      if (suite != null) {
+        Collection<OCStructSymbol> res =
+            CidrTestUtil.findGoogleTestSymbolsForSuiteRandomly(
+                element.getProject(), suite.first, true);
+        if (res.size() != 0) {
+          OCStruct struct = (OCStruct) res.iterator().next().locateDefinition();
+          return TestTarget.createFromClassAndMethod(struct, suite.first, null);
+        }
+      }
+    } else if (parent instanceof OCFile) {
+      return TestTarget.createFromFile(parent);
+    }
+    return null;
+  }
+
+  private static boolean isFirstArgument(@Nullable PsiElement element) {
+    OCMacroCall macroCall = PsiTreeUtil.getParentOfType(element, OCMacroCall.class);
+    if (macroCall != null) {
+      List<OCMacroCallArgument> arguments = macroCall.getArguments();
+      return arguments.size() > 0 && arguments.get(0).equals(element);
+    }
+    return false;
+  }
+
+  @Nullable
+  private static Label getCcTestTarget(@Nullable PsiElement element) {
+    if (element == null) {
+      return null;
+    }
+    File file = getContainingFile(element);
+    if (file == null) {
+      return null;
+    }
+    Collection<RuleIdeInfo> rules =
+        TestRuleFinder.getInstance(element.getProject()).testTargetsForSourceFile(file);
+    return TestRuleHeuristic.chooseTestTargetForSourceFile(file, rules, null);
+  }
+
+  private 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;
+  }
+}
diff --git a/clwb/src/com/google/idea/blaze/clwb/sync/BlazeCLionSyncPlugin.java b/clwb/src/com/google/idea/blaze/clwb/sync/BlazeCLionSyncPlugin.java
new file mode 100644
index 0000000..c387715
--- /dev/null
+++ b/clwb/src/com/google/idea/blaze/clwb/sync/BlazeCLionSyncPlugin.java
@@ -0,0 +1,65 @@
+/*
+ * Copyright 2016 The Bazel Authors. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.idea.blaze.clwb.sync;
+
+import com.google.idea.blaze.base.model.BlazeProjectData;
+import com.google.idea.blaze.base.model.primitives.WorkspaceRoot;
+import com.google.idea.blaze.base.model.primitives.WorkspaceType;
+import com.google.idea.blaze.base.projectview.ProjectViewSet;
+import com.google.idea.blaze.base.scope.BlazeContext;
+import com.google.idea.blaze.base.sync.BlazeSyncPlugin;
+import com.intellij.openapi.module.ModuleType;
+import com.intellij.openapi.project.Project;
+import com.intellij.openapi.roots.ContentEntry;
+import com.intellij.openapi.vfs.VirtualFile;
+import com.jetbrains.cidr.cpp.CPPModuleType;
+import java.util.Collection;
+import javax.annotation.Nullable;
+
+class BlazeCLionSyncPlugin extends BlazeSyncPlugin.Adapter {
+
+  @Nullable
+  @Override
+  public WorkspaceType getDefaultWorkspaceType() {
+    return WorkspaceType.C;
+  }
+
+  @Nullable
+  @Override
+  public ModuleType getWorkspaceModuleType(WorkspaceType workspaceType) {
+    if (workspaceType == WorkspaceType.C) {
+      return CPPModuleType.getInstance();
+    }
+    return null;
+  }
+
+  @Override
+  public void updateContentEntries(
+      Project project,
+      BlazeContext context,
+      WorkspaceRoot workspaceRoot,
+      ProjectViewSet projectViewSet,
+      BlazeProjectData blazeProjectData,
+      Collection<ContentEntry> contentEntries) {
+
+    for (ContentEntry entry : contentEntries) {
+      VirtualFile file = entry.getFile();
+      if (file != null) {
+        entry.addSourceFolder(file, false);
+      }
+    }
+  }
+}
diff --git a/clwb/src/com/google/idea/blaze/clwb/wizard2/BlazeCProjectCreator.java b/clwb/src/com/google/idea/blaze/clwb/wizard2/BlazeCProjectCreator.java
new file mode 100644
index 0000000..ed330ca
--- /dev/null
+++ b/clwb/src/com/google/idea/blaze/clwb/wizard2/BlazeCProjectCreator.java
@@ -0,0 +1,150 @@
+/*
+ * Copyright 2016 The Bazel Authors. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.idea.blaze.clwb.wizard2;
+
+import com.intellij.ide.impl.ProjectUtil;
+import com.intellij.ide.util.projectWizard.ProjectBuilder;
+import com.intellij.ide.util.projectWizard.WizardContext;
+import com.intellij.openapi.application.ApplicationManager;
+import com.intellij.openapi.application.ModalityState;
+import com.intellij.openapi.components.StorageScheme;
+import com.intellij.openapi.diagnostic.Logger;
+import com.intellij.openapi.project.Project;
+import com.intellij.openapi.project.ex.ProjectManagerEx;
+import com.intellij.openapi.roots.ui.configuration.ModulesProvider;
+import com.intellij.openapi.startup.StartupManager;
+import com.intellij.openapi.ui.Messages;
+import com.intellij.openapi.util.io.FileUtil;
+import com.intellij.openapi.wm.IdeFocusManager;
+import com.intellij.openapi.wm.IdeFrame;
+import com.intellij.openapi.wm.ToolWindow;
+import com.intellij.openapi.wm.ToolWindowId;
+import com.intellij.openapi.wm.ToolWindowManager;
+import com.intellij.openapi.wm.WindowManager;
+import com.intellij.openapi.wm.ex.IdeFrameEx;
+import com.intellij.openapi.wm.impl.IdeFrameImpl;
+import com.intellij.util.ui.UIUtil;
+import java.io.File;
+import java.io.IOException;
+import javax.annotation.Nullable;
+import javax.swing.SwingUtilities;
+
+class BlazeCProjectCreator {
+  private static final Logger LOG = Logger.getInstance(BlazeCProjectCreator.class);
+
+  private final WizardContext wizardContext;
+  private final ProjectBuilder projectBuilder;
+
+  public BlazeCProjectCreator(WizardContext wizardContext, ProjectBuilder projectBuilder) {
+    this.wizardContext = wizardContext;
+    this.projectBuilder = projectBuilder;
+  }
+
+  @Nullable
+  public Project createFromWizard() {
+    try {
+      return doCreate();
+    } catch (final IOException e) {
+      UIUtil.invokeLaterIfNeeded(
+          () -> Messages.showErrorDialog(e.getMessage(), "Project Initialization Failed"));
+      return null;
+    }
+  }
+
+  @Nullable
+  private Project doCreate() throws IOException {
+    final ProjectManagerEx projectManager = ProjectManagerEx.getInstanceEx();
+    final String projectFilePath = wizardContext.getProjectFileDirectory();
+
+    try {
+      File projectDir = new File(projectFilePath).getParentFile();
+      LOG.assertTrue(
+          projectDir != null,
+          "Cannot create project in '" + projectFilePath + "': no parent file exists");
+      FileUtil.ensureExists(projectDir);
+      if (wizardContext.getProjectStorageFormat() == StorageScheme.DIRECTORY_BASED) {
+        final File ideaDir = new File(projectFilePath, Project.DIRECTORY_STORE_FOLDER);
+        FileUtil.ensureExists(ideaDir);
+      }
+
+      String name = wizardContext.getProjectName();
+      Project newProject = projectBuilder.createProject(name, projectFilePath);
+      if (newProject == null) {
+        return null;
+      }
+
+      if (!ApplicationManager.getApplication().isUnitTestMode()) {
+        newProject.save();
+      }
+
+      if (!projectBuilder.validate(null, newProject)) {
+        return null;
+      }
+
+      projectBuilder.commit(newProject, null, ModulesProvider.EMPTY_MODULES_PROVIDER);
+
+      StartupManager.getInstance(newProject)
+          .registerPostStartupActivity(
+              () -> {
+                // ensure the dialog is shown after all startup activities are done
+                //noinspection SSBasedInspection
+                SwingUtilities.invokeLater(
+                    () -> {
+                      if (newProject.isDisposed()
+                          || ApplicationManager.getApplication().isUnitTestMode()) {
+                        return;
+                      }
+                      ApplicationManager.getApplication()
+                          .invokeLater(
+                              () -> {
+                                if (newProject.isDisposed()) {
+                                  return;
+                                }
+                                final ToolWindow toolWindow =
+                                    ToolWindowManager.getInstance(newProject)
+                                        .getToolWindow(ToolWindowId.PROJECT_VIEW);
+                                if (toolWindow != null) {
+                                  toolWindow.activate(null);
+                                }
+                              },
+                              ModalityState.NON_MODAL);
+                    });
+              });
+
+      ProjectUtil.updateLastProjectLocation(projectFilePath);
+
+      if (WindowManager.getInstance().isFullScreenSupportedInCurrentOS()) {
+        IdeFocusManager instance = IdeFocusManager.findInstance();
+        IdeFrame lastFocusedFrame = instance.getLastFocusedFrame();
+        if (lastFocusedFrame instanceof IdeFrameEx) {
+          boolean fullScreen = ((IdeFrameEx) lastFocusedFrame).isInFullScreen();
+          if (fullScreen) {
+            newProject.putUserData(IdeFrameImpl.SHOULD_OPEN_IN_FULL_SCREEN, Boolean.TRUE);
+          }
+        }
+      }
+
+      projectManager.openProject(newProject);
+
+      if (!ApplicationManager.getApplication().isUnitTestMode()) {
+        newProject.save();
+      }
+      return newProject;
+    } finally {
+      projectBuilder.cleanup();
+    }
+  }
+}
diff --git a/clwb/src/com/google/idea/blaze/clwb/wizard2/BlazeEditProjectViewImportWizardStep.java b/clwb/src/com/google/idea/blaze/clwb/wizard2/BlazeEditProjectViewImportWizardStep.java
new file mode 100644
index 0000000..2998d4a
--- /dev/null
+++ b/clwb/src/com/google/idea/blaze/clwb/wizard2/BlazeEditProjectViewImportWizardStep.java
@@ -0,0 +1,94 @@
+/*
+ * Copyright 2016 The Bazel Authors. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.idea.blaze.clwb.wizard2;
+
+import com.google.idea.blaze.base.ui.BlazeValidationResult;
+import com.google.idea.blaze.base.wizard2.BlazeNewProjectBuilder;
+import com.google.idea.blaze.base.wizard2.BlazeProjectCommitException;
+import com.google.idea.blaze.base.wizard2.ui.BlazeEditProjectViewControl;
+import com.intellij.ide.util.projectWizard.WizardContext;
+import com.intellij.ide.wizard.CommitStepException;
+import com.intellij.openapi.options.ConfigurationException;
+import java.awt.BorderLayout;
+import javax.swing.JComponent;
+import javax.swing.JPanel;
+import org.jetbrains.annotations.NotNull;
+
+/** Shows the edit project view screen. */
+class BlazeEditProjectViewImportWizardStep extends ProjectImportWizardStep {
+
+  private final JPanel component = new JPanel(new BorderLayout());
+  private BlazeEditProjectViewControl control;
+  private boolean settingsInitialised;
+
+  public BlazeEditProjectViewImportWizardStep(@NotNull WizardContext context) {
+    super(context);
+  }
+
+  @Override
+  public JComponent getComponent() {
+    return component;
+  }
+
+  @Override
+  public void updateStep() {
+    if (!settingsInitialised) {
+      init();
+    } else {
+      control.update(getProjectBuilder());
+    }
+  }
+
+  private void init() {
+    control =
+        new BlazeEditProjectViewControl(getProjectBuilder(), getWizardContext().getDisposable());
+    this.component.add(control.getUiComponent());
+    settingsInitialised = true;
+  }
+
+  @Override
+  public boolean validate() throws ConfigurationException {
+    BlazeValidationResult validationResult = control.validate();
+    if (validationResult.error != null) {
+      throw new ConfigurationException(validationResult.error.getError());
+    }
+    return validationResult.success;
+  }
+
+  @Override
+  public void updateDataModel() {
+    BlazeNewProjectBuilder builder = getProjectBuilder();
+    control.updateBuilder(builder);
+
+    WizardContext wizardContext = getWizardContext();
+    wizardContext.setProjectName(builder.getProjectName());
+    wizardContext.setProjectFileDirectory(builder.getProjectDataDirectory());
+  }
+
+  @Override
+  public void onWizardFinished() throws CommitStepException {
+    try {
+      getProjectBuilder().commit();
+    } catch (BlazeProjectCommitException e) {
+      throw new CommitStepException(e.getMessage());
+    }
+  }
+
+  @Override
+  public String getHelpId() {
+    return "docs/project-views.md";
+  }
+}
diff --git a/clwb/src/com/google/idea/blaze/clwb/wizard2/BlazeImportProjectAction.java b/clwb/src/com/google/idea/blaze/clwb/wizard2/BlazeImportProjectAction.java
new file mode 100644
index 0000000..76148dd
--- /dev/null
+++ b/clwb/src/com/google/idea/blaze/clwb/wizard2/BlazeImportProjectAction.java
@@ -0,0 +1,52 @@
+/*
+ * Copyright 2016 The Bazel Authors. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.idea.blaze.clwb.wizard2;
+
+import com.google.idea.blaze.base.settings.Blaze;
+import com.intellij.ide.util.projectWizard.WizardContext;
+import com.intellij.openapi.actionSystem.AnAction;
+import com.intellij.openapi.actionSystem.AnActionEvent;
+
+class BlazeImportProjectAction extends AnAction {
+
+  @Override
+  public void actionPerformed(AnActionEvent e) {
+    BlazeNewProjectWizard wizard =
+        new BlazeNewProjectWizard() {
+          @Override
+          protected ProjectImportWizardStep[] getSteps(WizardContext context) {
+            return new ProjectImportWizardStep[] {
+              new BlazeSelectWorkspaceImportWizardStep(context),
+              new BlazeSelectBuildSystemBinaryStep(context),
+              new BlazeSelectProjectViewImportWizardStep(context),
+              new BlazeEditProjectViewImportWizardStep(context)
+            };
+          }
+        };
+    if (!wizard.showAndGet()) {
+      return;
+    }
+    BlazeCProjectCreator projectCreator = new BlazeCProjectCreator(wizard.context, wizard.builder);
+    projectCreator.createFromWizard();
+  }
+
+  @Override
+  public void update(AnActionEvent e) {
+    super.update(e);
+    e.getPresentation()
+        .setText(String.format("Import %s Project...", Blaze.defaultBuildSystemName()));
+  }
+}
diff --git a/clwb/src/com/google/idea/blaze/clwb/wizard2/BlazeNewProjectWizard.java b/clwb/src/com/google/idea/blaze/clwb/wizard2/BlazeNewProjectWizard.java
new file mode 100644
index 0000000..d323603
--- /dev/null
+++ b/clwb/src/com/google/idea/blaze/clwb/wizard2/BlazeNewProjectWizard.java
@@ -0,0 +1,210 @@
+/*
+ * Copyright 2016 The Bazel Authors. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.idea.blaze.clwb.wizard2;
+
+import com.google.idea.blaze.base.help.BlazeHelpHandler;
+import com.google.idea.blaze.base.settings.Blaze;
+import com.intellij.ide.util.projectWizard.WizardContext;
+import com.intellij.ide.wizard.AbstractWizard;
+import com.intellij.ide.wizard.CommitStepException;
+import com.intellij.openapi.options.ConfigurationException;
+import com.intellij.openapi.project.DumbModePermission;
+import com.intellij.openapi.project.DumbService;
+import com.intellij.openapi.project.Project;
+import com.intellij.openapi.ui.Messages;
+import com.intellij.openapi.util.Ref;
+import java.awt.event.ActionListener;
+import javax.annotation.Nullable;
+
+/** Largely copied from AbstractProjectWizard / AddModuleWizard (which aren't in the CLion SDK). */
+abstract class BlazeNewProjectWizard extends AbstractWizard<ProjectImportWizardStep> {
+
+  final WizardContext context;
+  final BlazeProjectImportBuilder builder;
+
+  public BlazeNewProjectWizard() {
+    super("Import Project from " + Blaze.defaultBuildSystemName(), (Project) null);
+
+    builder = new BlazeProjectImportBuilder();
+    context = new WizardContext(null, getDisposable());
+    context.setWizard(this);
+    context.setProjectBuilder(builder);
+    for (ProjectImportWizardStep step : getSteps(context)) {
+      addStep(step);
+    }
+    init();
+  }
+
+  protected abstract ProjectImportWizardStep[] getSteps(WizardContext context);
+
+  @Override
+  protected void helpAction() {
+    doHelpAction();
+  }
+
+  @Override
+  protected void doHelpAction() {
+    String helpId = getHelpID();
+    BlazeHelpHandler helpHandler = BlazeHelpHandler.getInstance();
+    if (helpId != null && helpHandler != null) {
+      helpHandler.handleHelp(helpId);
+    }
+  }
+
+  @Nullable
+  @Override
+  protected String getHelpID() {
+    ProjectImportWizardStep step = getCurrentStepObject();
+    if (step != null) {
+      return step.getHelpId();
+    }
+    return null;
+  }
+
+  // Swallow the escape key
+  @Nullable
+  @Override
+  protected ActionListener createCancelAction() {
+    return null;
+  }
+
+  @Override
+  protected void updateStep() {
+    if (!mySteps.isEmpty()) {
+      getCurrentStepObject().updateStep();
+    }
+    super.updateStep();
+    myIcon.setIcon(null);
+  }
+
+  @Override
+  protected final void doOKAction() {
+    final Ref<Boolean> result = Ref.create(false);
+    DumbService.allowStartingDumbModeInside(
+        DumbModePermission.MAY_START_BACKGROUND,
+        new Runnable() {
+          @Override
+          public void run() {
+            result.set(doFinishAction());
+          }
+        });
+    if (!result.get()) {
+      return;
+    }
+    super.doOKAction();
+  }
+
+  private boolean doFinishAction() {
+    int idx = getCurrentStep();
+    try {
+      do {
+        ProjectImportWizardStep step = mySteps.get(idx);
+        if (step != getCurrentStepObject()) {
+          step.updateStep();
+        }
+        if (!commitStepData(step)) {
+          return false;
+        }
+        try {
+          step._commit(true);
+        } catch (CommitStepException e) {
+          handleCommitException(e);
+          return false;
+        }
+        if (!isLastStep(idx)) {
+          idx = getNextStep(idx);
+        } else {
+          for (ProjectImportWizardStep wizardStep : mySteps) {
+            try {
+              wizardStep.onWizardFinished();
+            } catch (CommitStepException e) {
+              handleCommitException(e);
+              return false;
+            }
+          }
+          break;
+        }
+      } while (true);
+    } finally {
+      myCurrentStep = idx;
+      updateStep();
+    }
+    return true;
+  }
+
+  private boolean commitStepData(ProjectImportWizardStep step) {
+    try {
+      if (!step.validate()) {
+        return false;
+      }
+    } catch (ConfigurationException e) {
+      Messages.showErrorDialog(myContentPanel, e.getMessage(), e.getTitle());
+      return false;
+    }
+    step.updateDataModel();
+    return true;
+  }
+
+  @Override
+  public void doNextAction() {
+    if (!commitStepData(getCurrentStepObject())) {
+      return;
+    }
+    super.doNextAction();
+  }
+
+  private void handleCommitException(CommitStepException e) {
+    String message = e.getMessage();
+    if (message != null) {
+      Messages.showErrorDialog(getCurrentStepComponent(), message);
+    }
+  }
+
+  @Override
+  protected boolean isLastStep() {
+    return isLastStep(getCurrentStep());
+  }
+
+  private boolean isLastStep(int step) {
+    return getNextStep(step) == step;
+  }
+
+  @Override
+  protected int getNextStep(int stepIndex) {
+    int nextIndex = stepIndex + 1;
+    while (nextIndex < mySteps.size()) {
+      ProjectImportWizardStep nextStep = mySteps.get(nextIndex);
+      if (nextStep.isStepVisible()) {
+        return nextIndex;
+      }
+      nextIndex++;
+    }
+    return stepIndex;
+  }
+
+  @Override
+  protected int getPreviousStep(int stepIndex) {
+    int prevIndex = stepIndex - 1;
+    while (prevIndex >= 0) {
+      ProjectImportWizardStep prevStep = mySteps.get(prevIndex);
+      if (prevStep.isStepVisible()) {
+        return prevIndex;
+      }
+      prevIndex--;
+    }
+    return stepIndex;
+  }
+}
diff --git a/clwb/src/com/google/idea/blaze/clwb/wizard2/BlazeProjectImportBuilder.java b/clwb/src/com/google/idea/blaze/clwb/wizard2/BlazeProjectImportBuilder.java
new file mode 100644
index 0000000..58ef928
--- /dev/null
+++ b/clwb/src/com/google/idea/blaze/clwb/wizard2/BlazeProjectImportBuilder.java
@@ -0,0 +1,45 @@
+/*
+ * Copyright 2016 The Bazel Authors. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.idea.blaze.clwb.wizard2;
+
+import com.google.common.collect.ImmutableList;
+import com.google.idea.blaze.base.wizard2.BlazeNewProjectBuilder;
+import com.intellij.ide.util.projectWizard.ProjectBuilder;
+import com.intellij.openapi.module.ModifiableModuleModel;
+import com.intellij.openapi.module.Module;
+import com.intellij.openapi.project.Project;
+import com.intellij.openapi.roots.ui.configuration.ModulesProvider;
+import java.util.List;
+import javax.annotation.Nullable;
+
+/** Wrapper around a {@link BlazeNewProjectBuilder} to fit into IntelliJ's import framework. */
+class BlazeProjectImportBuilder extends ProjectBuilder {
+  private BlazeNewProjectBuilder builder = new BlazeNewProjectBuilder();
+
+  @Nullable
+  @Override
+  public List<Module> commit(
+      Project project,
+      @Nullable ModifiableModuleModel modifiableModuleModel,
+      ModulesProvider modulesProvider) {
+    builder.commitToProject(project);
+    return ImmutableList.of();
+  }
+
+  public BlazeNewProjectBuilder builder() {
+    return builder;
+  }
+}
diff --git a/clwb/src/com/google/idea/blaze/clwb/wizard2/BlazeSelectBuildSystemBinaryStep.java b/clwb/src/com/google/idea/blaze/clwb/wizard2/BlazeSelectBuildSystemBinaryStep.java
new file mode 100644
index 0000000..808fb63
--- /dev/null
+++ b/clwb/src/com/google/idea/blaze/clwb/wizard2/BlazeSelectBuildSystemBinaryStep.java
@@ -0,0 +1,92 @@
+/*
+ * Copyright 2016 The Bazel Authors. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.idea.blaze.clwb.wizard2;
+
+import com.google.idea.blaze.base.settings.Blaze.BuildSystem;
+import com.google.idea.blaze.base.settings.BlazeUserSettings;
+import com.google.idea.blaze.base.ui.BlazeValidationResult;
+import com.google.idea.blaze.base.wizard2.ui.SelectBazelBinaryControl;
+import com.intellij.ide.util.projectWizard.WizardContext;
+import com.intellij.ide.wizard.CommitStepException;
+import com.intellij.openapi.options.ConfigurationException;
+import java.awt.BorderLayout;
+import javax.annotation.Nullable;
+import javax.swing.JComponent;
+import javax.swing.JPanel;
+import org.jetbrains.annotations.NotNull;
+
+class BlazeSelectBuildSystemBinaryStep extends
+    com.google.idea.blaze.clwb.wizard2.ProjectImportWizardStep {
+
+  private final JPanel component = new JPanel(new BorderLayout());
+  private SelectBazelBinaryControl control;
+  private boolean settingsInitialized = false;
+
+  public BlazeSelectBuildSystemBinaryStep(@NotNull WizardContext context) {
+    super(context);
+  }
+
+  @Override
+  public boolean isStepVisible() {
+    updateStep();
+    if (control.builder.getBuildSystem() != BuildSystem.Bazel) {
+      return false;
+    }
+    String currentBinaryPath = BlazeUserSettings.getInstance().getBazelBinaryPath();
+    return currentBinaryPath == null;
+  }
+
+  @Override
+  public JComponent getComponent() {
+    return component;
+  }
+
+  @Override
+  public void updateStep() {
+    if (!settingsInitialized) {
+      init();
+    }
+  }
+
+  private void init() {
+    control = new SelectBazelBinaryControl(getProjectBuilder());
+    component.add(control.getUiComponent());
+    settingsInitialized = true;
+  }
+
+  @Override
+  public boolean validate() throws ConfigurationException {
+    BlazeValidationResult result = control.validate();
+    if (!result.success) {
+      throw new ConfigurationException(result.error.getError());
+    }
+    return true;
+  }
+
+  @Override
+  public void updateDataModel() {}
+
+  @Override
+  public void onWizardFinished() throws CommitStepException {
+    control.commit();
+  }
+
+  @Nullable
+  @Override
+  public String getHelpId() {
+    return null;
+  }
+}
diff --git a/clwb/src/com/google/idea/blaze/clwb/wizard2/BlazeSelectProjectViewImportWizardStep.java b/clwb/src/com/google/idea/blaze/clwb/wizard2/BlazeSelectProjectViewImportWizardStep.java
new file mode 100644
index 0000000..9c99539
--- /dev/null
+++ b/clwb/src/com/google/idea/blaze/clwb/wizard2/BlazeSelectProjectViewImportWizardStep.java
@@ -0,0 +1,81 @@
+/*
+ * Copyright 2016 The Bazel Authors. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.idea.blaze.clwb.wizard2;
+
+import com.google.idea.blaze.base.ui.BlazeValidationResult;
+import com.google.idea.blaze.base.wizard2.ui.BlazeSelectProjectViewControl;
+import com.intellij.ide.util.projectWizard.WizardContext;
+import com.intellij.ide.wizard.CommitStepException;
+import com.intellij.openapi.options.ConfigurationException;
+import java.awt.BorderLayout;
+import javax.swing.JComponent;
+import javax.swing.JPanel;
+import org.jetbrains.annotations.NotNull;
+
+class BlazeSelectProjectViewImportWizardStep extends ProjectImportWizardStep {
+
+  private final JPanel component = new JPanel(new BorderLayout());
+  private BlazeSelectProjectViewControl control;
+  private boolean settingsInitialised;
+
+  public BlazeSelectProjectViewImportWizardStep(@NotNull WizardContext context) {
+    super(context);
+  }
+
+  @Override
+  public JComponent getComponent() {
+    return component;
+  }
+
+  @Override
+  public void updateStep() {
+    if (!settingsInitialised) {
+      init();
+    } else {
+      control.update(getProjectBuilder());
+    }
+  }
+
+  private void init() {
+    control = new BlazeSelectProjectViewControl(getProjectBuilder());
+    this.component.add(control.getUiComponent());
+    settingsInitialised = true;
+  }
+
+  @Override
+  public boolean validate() throws ConfigurationException {
+    BlazeValidationResult result = control.validate();
+    if (!result.success) {
+      throw new ConfigurationException(result.error.getError());
+    }
+    return true;
+  }
+
+  @Override
+  public void updateDataModel() {
+    control.updateBuilder(getProjectBuilder());
+  }
+
+  @Override
+  public void onWizardFinished() throws CommitStepException {
+    control.commit();
+  }
+
+  @Override
+  public String getHelpId() {
+    return "docs/project-views.md";
+  }
+}
diff --git a/clwb/src/com/google/idea/blaze/clwb/wizard2/BlazeSelectWorkspaceImportWizardStep.java b/clwb/src/com/google/idea/blaze/clwb/wizard2/BlazeSelectWorkspaceImportWizardStep.java
new file mode 100644
index 0000000..2b1666e
--- /dev/null
+++ b/clwb/src/com/google/idea/blaze/clwb/wizard2/BlazeSelectWorkspaceImportWizardStep.java
@@ -0,0 +1,80 @@
+/*
+ * Copyright 2016 The Bazel Authors. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.idea.blaze.clwb.wizard2;
+
+import com.google.idea.blaze.base.ui.BlazeValidationResult;
+import com.google.idea.blaze.base.wizard2.ui.BlazeSelectWorkspaceControl;
+import com.intellij.ide.util.projectWizard.WizardContext;
+import com.intellij.ide.wizard.CommitStepException;
+import com.intellij.openapi.options.ConfigurationException;
+import java.awt.BorderLayout;
+import javax.swing.JComponent;
+import javax.swing.JPanel;
+import org.jetbrains.annotations.NotNull;
+
+class BlazeSelectWorkspaceImportWizardStep extends
+    com.google.idea.blaze.clwb.wizard2.ProjectImportWizardStep {
+
+  private final JPanel component = new JPanel(new BorderLayout());
+  private BlazeSelectWorkspaceControl control;
+  private boolean settingsInitialised;
+
+  public BlazeSelectWorkspaceImportWizardStep(@NotNull WizardContext context) {
+    super(context);
+  }
+
+  @Override
+  public JComponent getComponent() {
+    return component;
+  }
+
+  @Override
+  public void updateStep() {
+    if (!settingsInitialised) {
+      init();
+    }
+  }
+
+  private void init() {
+    control = new BlazeSelectWorkspaceControl(getProjectBuilder());
+    this.component.add(control.getUiComponent());
+    settingsInitialised = true;
+  }
+
+  @Override
+  public boolean validate() throws ConfigurationException {
+    BlazeValidationResult result = control.validate();
+    if (!result.success) {
+      throw new ConfigurationException(result.error.getError());
+    }
+    return true;
+  }
+
+  @Override
+  public void updateDataModel() {
+    control.updateBuilder(getProjectBuilder());
+  }
+
+  @Override
+  public void onWizardFinished() throws CommitStepException {
+    control.commit();
+  }
+
+  @Override
+  public String getHelpId() {
+    return "docs/import-project.md";
+  }
+}
diff --git a/clwb/src/com/google/idea/blaze/clwb/wizard2/ProjectImportWizardStep.java b/clwb/src/com/google/idea/blaze/clwb/wizard2/ProjectImportWizardStep.java
new file mode 100644
index 0000000..392385d
--- /dev/null
+++ b/clwb/src/com/google/idea/blaze/clwb/wizard2/ProjectImportWizardStep.java
@@ -0,0 +1,70 @@
+/*
+ * Copyright 2016 The Bazel Authors. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.idea.blaze.clwb.wizard2;
+
+import com.google.idea.blaze.base.wizard2.BlazeNewProjectBuilder;
+import com.intellij.ide.util.projectWizard.ProjectBuilder;
+import com.intellij.ide.util.projectWizard.WizardContext;
+import com.intellij.ide.wizard.CommitStepException;
+import com.intellij.ide.wizard.StepAdapter;
+import com.intellij.openapi.options.ConfigurationException;
+import javax.annotation.Nullable;
+import javax.swing.Icon;
+
+abstract class ProjectImportWizardStep extends StepAdapter {
+  private final WizardContext myContext;
+
+  public ProjectImportWizardStep(WizardContext context) {
+    myContext = context;
+  }
+
+  @Override
+  public Icon getIcon() {
+    return myContext.getStepIcon();
+  }
+
+  protected ProjectBuilder getBuilder() {
+    return myContext.getProjectBuilder();
+  }
+
+  protected WizardContext getWizardContext() {
+    return myContext;
+  }
+
+  @Nullable
+  public abstract String getHelpId();
+
+  public boolean isStepVisible() {
+    return true;
+  }
+
+  /** Update UI from BlazeNewProjectBuilder and WizardContext */
+  public abstract void updateStep();
+
+  public abstract boolean validate() throws ConfigurationException;
+
+  /** Commits data from UI into BlazeNewProjectBuilder and WizardContext */
+  public abstract void updateDataModel();
+
+  public abstract void onWizardFinished() throws CommitStepException;
+
+  protected BlazeNewProjectBuilder getProjectBuilder() {
+    BlazeProjectImportBuilder builder =
+        (BlazeProjectImportBuilder) getWizardContext().getProjectBuilder();
+    assert builder != null;
+    return builder.builder();
+  }
+}
diff --git a/clwb/src/com/google/idea/blaze/plugin/ClwbPluginId.java b/clwb/src/com/google/idea/blaze/plugin/ClwbPluginId.java
new file mode 100644
index 0000000..c788fe5
--- /dev/null
+++ b/clwb/src/com/google/idea/blaze/plugin/ClwbPluginId.java
@@ -0,0 +1,36 @@
+/*
+ * 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.plugin;
+
+import com.google.idea.blaze.base.bazel.BuildSystemProvider;
+import com.google.idea.blaze.base.plugin.BlazePluginId;
+import com.google.idea.blaze.base.settings.Blaze.BuildSystem;
+
+/** IJwB plugin configuration information. */
+public class ClwbPluginId implements BlazePluginId {
+
+  static final String BLAZE_PLUGIN_ID = "com.google.idea.blaze.clwb";
+  static final String BAZEL_PLUGIN_ID = "com.google.idea.bazel.clwb";
+
+  @Override
+  public String getPluginId() {
+    BuildSystem type = BuildSystemProvider.defaultBuildSystem().buildSystem();
+    if (type == BuildSystem.Blaze) {
+      return BLAZE_PLUGIN_ID;
+    }
+    return BAZEL_PLUGIN_ID;
+  }
+}
diff --git a/clwb/src/com/google/idea/blaze/plugin/ClwbProjectSpecificInitializer.java b/clwb/src/com/google/idea/blaze/plugin/ClwbProjectSpecificInitializer.java
new file mode 100644
index 0000000..7be43e4
--- /dev/null
+++ b/clwb/src/com/google/idea/blaze/plugin/ClwbProjectSpecificInitializer.java
@@ -0,0 +1,40 @@
+/*
+ * Copyright 2016 The Bazel Authors. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.idea.blaze.plugin;
+
+import com.intellij.openapi.components.AbstractProjectComponent;
+import com.intellij.openapi.project.Project;
+import com.intellij.openapi.wm.ToolWindow;
+import com.intellij.openapi.wm.ToolWindowManager;
+import com.jetbrains.cidr.cpp.cmake.workspace.CMakeWorkspace;
+
+/** Runs on project startup, and customizes CLion UI. */
+public class ClwbProjectSpecificInitializer extends AbstractProjectComponent {
+
+  public ClwbProjectSpecificInitializer(Project project) {
+    super(project);
+  }
+
+  @Override
+  public void projectOpened() {
+    // removes the CMake tool window for blaze projects
+    ToolWindowManager manager = ToolWindowManager.getInstance(myProject);
+    ToolWindow tw = manager.getToolWindow(CMakeWorkspace.TOOLWINDOW_ID);
+    if (tw != null) {
+      tw.setAvailable(false, null);
+    }
+  }
+}
diff --git a/clwb/src/com/google/idea/blaze/plugin/ClwbSpecificInitializer.java b/clwb/src/com/google/idea/blaze/plugin/ClwbSpecificInitializer.java
new file mode 100644
index 0000000..4a3445e
--- /dev/null
+++ b/clwb/src/com/google/idea/blaze/plugin/ClwbSpecificInitializer.java
@@ -0,0 +1,44 @@
+/*
+ * 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.plugin;
+
+import com.google.idea.blaze.base.plugin.BlazeActionRemover;
+import com.intellij.openapi.components.ApplicationComponent;
+import com.jetbrains.cidr.cpp.cmake.actions.ChangeCMakeProjectContentRootAction;
+import com.jetbrains.cidr.cpp.cmake.actions.DropCMakeCacheAction;
+import com.jetbrains.cidr.cpp.cmake.actions.OpenCMakeSettingsAction;
+import com.jetbrains.cidr.cpp.cmake.actions.ReloadCMakeProjectAction;
+import com.jetbrains.cidr.cpp.cmake.actions.ToggleCMakeAutoReloadAction;
+
+/** Runs on startup. */
+public class ClwbSpecificInitializer extends ApplicationComponent.Adapter {
+
+  @Override
+  public void initComponent() {
+    hideCMakeActions();
+  }
+
+  // The original actions will be visible only on plain IDEA projects.
+  private static void hideCMakeActions() {
+    BlazeActionRemover.hideAction(ChangeCMakeProjectContentRootAction.ID);
+    BlazeActionRemover.hideAction(DropCMakeCacheAction.ID);
+    BlazeActionRemover.hideAction(OpenCMakeSettingsAction.ID);
+    BlazeActionRemover.hideAction(ReloadCMakeProjectAction.ID);
+    BlazeActionRemover.hideAction(ToggleCMakeAutoReloadAction.ID);
+    // 'CMake' > 'Show Generated CMake Files' action
+    BlazeActionRemover.hideAction("CMake.ShowGeneratedDir");
+  }
+}
diff --git a/common/experiments/BUILD b/common/experiments/BUILD
index b15de1f..8e7a91d 100644
--- a/common/experiments/BUILD
+++ b/common/experiments/BUILD
@@ -23,7 +23,7 @@
 )
 
 load(
-    "//intellij_test:test_defs.bzl",
+    "//testing:test_defs.bzl",
     "intellij_unit_test_suite",
 )
 
diff --git a/cpp/BUILD b/cpp/BUILD
index 8b23007..f38d357 100644
--- a/cpp/BUILD
+++ b/cpp/BUILD
@@ -38,7 +38,7 @@
 )
 
 load(
-    "//intellij_test:test_defs.bzl",
+    "//testing:test_defs.bzl",
     "intellij_unit_test_suite",
 )
 
diff --git a/cpp/src/com/google/idea/blaze/cpp/BlazeConfigurationResolver.java b/cpp/src/com/google/idea/blaze/cpp/BlazeConfigurationResolver.java
index 925f873..6204439 100644
--- a/cpp/src/com/google/idea/blaze/cpp/BlazeConfigurationResolver.java
+++ b/cpp/src/com/google/idea/blaze/cpp/BlazeConfigurationResolver.java
@@ -25,8 +25,8 @@
 import com.google.idea.blaze.base.async.executor.BlazeExecutor;
 import com.google.idea.blaze.base.ideinfo.CToolchainIdeInfo;
 import com.google.idea.blaze.base.ideinfo.RuleIdeInfo;
+import com.google.idea.blaze.base.ideinfo.RuleKey;
 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.LanguageClass;
 import com.google.idea.blaze.base.rulemaps.SourceToRuleMap;
 import com.google.idea.blaze.base.scope.BlazeContext;
@@ -52,11 +52,11 @@
 
 final class BlazeConfigurationResolver {
   private static final class MapEntry {
-    public final Label label;
+    public final RuleKey ruleKey;
     public final BlazeResolveConfiguration configuration;
 
-    public MapEntry(Label label, BlazeResolveConfiguration configuration) {
-      this.label = label;
+    public MapEntry(RuleKey ruleKey, BlazeResolveConfiguration configuration) {
+      this.ruleKey = ruleKey;
       this.configuration = configuration;
     }
   }
@@ -64,7 +64,8 @@
   private static final Logger LOG = Logger.getInstance(BlazeConfigurationResolver.class);
   private final Project project;
 
-  private ImmutableMap<Label, BlazeResolveConfiguration> resolveConfigurations = ImmutableMap.of();
+  private ImmutableMap<RuleKey, BlazeResolveConfiguration> resolveConfigurations =
+      ImmutableMap.of();
 
   public BlazeConfigurationResolver(Project project) {
     this.project = project;
@@ -72,7 +73,7 @@
 
   public void update(BlazeContext context, BlazeProjectData blazeProjectData) {
     WorkspacePathResolver workspacePathResolver = blazeProjectData.workspacePathResolver;
-    ImmutableMap<Label, CToolchainIdeInfo> toolchainLookupMap =
+    ImmutableMap<RuleKey, CToolchainIdeInfo> toolchainLookupMap =
         BlazeResolveConfiguration.buildToolchainLookupMap(
             context, blazeProjectData.ruleMap, blazeProjectData.reverseDependencies);
     resolveConfigurations =
@@ -80,15 +81,15 @@
             context, blazeProjectData, toolchainLookupMap, workspacePathResolver);
   }
 
-  private ImmutableMap<Label, BlazeResolveConfiguration> buildBlazeConfigurationMap(
+  private ImmutableMap<RuleKey, BlazeResolveConfiguration> buildBlazeConfigurationMap(
       BlazeContext parentContext,
       BlazeProjectData blazeProjectData,
-      ImmutableMap<Label, CToolchainIdeInfo> toolchainLookupMap,
+      ImmutableMap<RuleKey, CToolchainIdeInfo> toolchainLookupMap,
       WorkspacePathResolver workspacePathResolver) {
     // Type specification needed to avoid incorrect type inference during command line build.
     return Scope.push(
         parentContext,
-        (ScopedFunction<ImmutableMap<Label, BlazeResolveConfiguration>>)
+        (ScopedFunction<ImmutableMap<RuleKey, BlazeResolveConfiguration>>)
             context -> {
               context.push(new TimingScope("Build C configuration map"));
 
@@ -110,7 +111,7 @@
                 }
               }
 
-              ImmutableMap.Builder<Label, BlazeResolveConfiguration> newResolveConfigurations =
+              ImmutableMap.Builder<RuleKey, BlazeResolveConfiguration> newResolveConfigurations =
                   ImmutableMap.builder();
               List<MapEntry> mapEntries;
               try {
@@ -125,7 +126,7 @@
               for (MapEntry mapEntry : mapEntries) {
                 // Skip over labels that don't have C configuration data.
                 if (mapEntry != null) {
-                  newResolveConfigurations.put(mapEntry.label, mapEntry.configuration);
+                  newResolveConfigurations.put(mapEntry.ruleKey, mapEntry.configuration);
                 }
               }
               return newResolveConfigurations.build();
@@ -139,27 +140,28 @@
   @Nullable
   private MapEntry createResolveConfiguration(
       RuleIdeInfo rule,
-      ImmutableMap<Label, CToolchainIdeInfo> toolchainLookupMap,
+      ImmutableMap<RuleKey, CToolchainIdeInfo> toolchainLookupMap,
       ConcurrentMap<CToolchainIdeInfo, File> compilerWrapperCache,
       WorkspacePathResolver workspacePathResolver,
       BlazeProjectData blazeProjectData) {
-    Label label = rule.label;
-    LOG.info("Resolving " + label.toString());
-    CToolchainIdeInfo toolchainIdeInfo = toolchainLookupMap.get(label);
+    RuleKey ruleKey = rule.key;
+    LOG.info("Resolving " + ruleKey);
+
+    CToolchainIdeInfo toolchainIdeInfo = toolchainLookupMap.get(ruleKey);
     if (toolchainIdeInfo != null) {
       File compilerWrapper =
           findOrCreateCompilerWrapperScript(
-              compilerWrapperCache, toolchainIdeInfo, workspacePathResolver, rule.label);
+              compilerWrapperCache, toolchainIdeInfo, workspacePathResolver, ruleKey);
       if (compilerWrapper != null) {
         BlazeResolveConfiguration config =
             BlazeResolveConfiguration.createConfigurationForTarget(
                 project,
                 workspacePathResolver,
-                blazeProjectData.ruleMap.get(label),
+                blazeProjectData.ruleMap.get(ruleKey),
                 toolchainIdeInfo,
                 compilerWrapper);
         if (config != null) {
-          return new MapEntry(label, config);
+          return new MapEntry(ruleKey, config);
         }
       }
     }
@@ -171,7 +173,7 @@
       Map<CToolchainIdeInfo, File> compilerWrapperCache,
       CToolchainIdeInfo toolchainIdeInfo,
       WorkspacePathResolver workspacePathResolver,
-      Label label) {
+      RuleKey ruleKey) {
     File compilerWrapper = compilerWrapperCache.get(toolchainIdeInfo);
     if (compilerWrapper == null) {
       File cppExecutable = toolchainIdeInfo.cppExecutable.getAbsoluteOrRelativeFile();
@@ -182,7 +184,7 @@
         String errorMessage =
             String.format(
                 "Unable to find compiler executable: %s for rule %s",
-                toolchainIdeInfo.cppExecutable.toString(), label.toString());
+                toolchainIdeInfo.cppExecutable.toString(), ruleKey);
         LOG.warn(errorMessage);
         compilerWrapper = null;
       } else {
@@ -251,9 +253,9 @@
   @Nullable
   public OCResolveConfiguration getConfigurationForFile(VirtualFile sourceFile) {
     SourceToRuleMap sourceToRuleMap = SourceToRuleMap.getInstance(project);
-    List<Label> targetsForSourceFile =
+    List<RuleKey> targetsForSourceFile =
         Lists.newArrayList(
-            sourceToRuleMap.getTargetsForSourceFile(VfsUtilCore.virtualToIoFile(sourceFile)));
+            sourceToRuleMap.getRulesForSourceFile(VfsUtilCore.virtualToIoFile(sourceFile)));
     if (targetsForSourceFile.isEmpty()) {
       return null;
     }
@@ -262,10 +264,10 @@
     // we can't possibly show how it will be interpreted in both contexts at the same time
     // in the IDE, so just pick the first target after we sort.
     targetsForSourceFile.sort((o1, o2) -> o1.toString().compareTo(o2.toString()));
-    Label target = Iterables.getFirst(targetsForSourceFile, null);
-    assert (target != null);
+    RuleKey ruleKey = Iterables.getFirst(targetsForSourceFile, null);
+    assert (ruleKey != null);
 
-    return resolveConfigurations.get(target);
+    return resolveConfigurations.get(ruleKey);
   }
 
   public List<? extends OCResolveConfiguration> getAllConfigurations() {
diff --git a/cpp/src/com/google/idea/blaze/cpp/BlazeResolveConfigurationTemporaryBase.java b/cpp/src/com/google/idea/blaze/cpp/BlazeResolveConfigurationTemporaryBase.java
index a710707..ef7b9c0 100644
--- a/cpp/src/com/google/idea/blaze/cpp/BlazeResolveConfigurationTemporaryBase.java
+++ b/cpp/src/com/google/idea/blaze/cpp/BlazeResolveConfigurationTemporaryBase.java
@@ -25,9 +25,9 @@
 import com.google.idea.blaze.base.ideinfo.CRuleIdeInfo;
 import com.google.idea.blaze.base.ideinfo.CToolchainIdeInfo;
 import com.google.idea.blaze.base.ideinfo.RuleIdeInfo;
-import com.google.idea.blaze.base.model.RuleMap;
+import com.google.idea.blaze.base.ideinfo.RuleKey;
+import com.google.idea.blaze.base.ideinfo.RuleMap;
 import com.google.idea.blaze.base.model.primitives.ExecutionRootPath;
-import com.google.idea.blaze.base.model.primitives.Label;
 import com.google.idea.blaze.base.scope.BlazeContext;
 import com.google.idea.blaze.base.scope.Scope;
 import com.google.idea.blaze.base.scope.scopes.TimingScope;
@@ -72,7 +72,7 @@
 
   /* project, label are protected instead of private just so v145 can access */
   protected final Project project;
-  protected final Label label;
+  protected final RuleKey ruleKey;
 
   private final ImmutableList<HeadersSearchRoot> cLibraryIncludeRoots;
   private final ImmutableList<HeadersSearchRoot> cppLibraryIncludeRoots;
@@ -120,7 +120,7 @@
     return new BlazeResolveConfiguration(
         project,
         workspacePathResolver,
-        ruleIdeInfo.label,
+        ruleIdeInfo.key,
         systemIncludesBuilder.build(),
         systemIncludesBuilder.build(),
         userQuoteIncludesBuilder.build(),
@@ -134,30 +134,31 @@
         cppFlagsBuilder.build());
   }
 
-  public static ImmutableMap<Label, CToolchainIdeInfo> buildToolchainLookupMap(
-      BlazeContext context, RuleMap ruleMap, ImmutableMultimap<Label, Label> reverseDependencies) {
+  public static ImmutableMap<RuleKey, CToolchainIdeInfo> buildToolchainLookupMap(
+      BlazeContext context,
+      RuleMap ruleMap,
+      ImmutableMultimap<RuleKey, RuleKey> reverseDependencies) {
     return Scope.push(
         context,
         childContext -> {
           childContext.push(new TimingScope("Build toolchain lookup map"));
 
-          List<Label> seeds = Lists.newArrayList();
+          List<RuleKey> seeds = Lists.newArrayList();
           for (RuleIdeInfo rule : ruleMap.rules()) {
-            Label label = rule.label;
             CToolchainIdeInfo cToolchainIdeInfo = rule.cToolchainIdeInfo;
             if (cToolchainIdeInfo != null) {
-              seeds.add(label);
+              seeds.add(rule.key);
             }
           }
 
-          Map<Label, CToolchainIdeInfo> lookupTable = Maps.newHashMap();
-          for (Label seed : seeds) {
+          Map<RuleKey, CToolchainIdeInfo> lookupTable = Maps.newHashMap();
+          for (RuleKey seed : seeds) {
             CToolchainIdeInfo toolchainInfo = ruleMap.get(seed).cToolchainIdeInfo;
             LOG.assertTrue(toolchainInfo != null);
-            List<Label> worklist = Lists.newArrayList(reverseDependencies.get(seed));
+            List<RuleKey> worklist = Lists.newArrayList(reverseDependencies.get(seed));
             while (!worklist.isEmpty()) {
               // We should never see a label depend on two different toolchains.
-              Label l = worklist.remove(0);
+              RuleKey l = worklist.remove(0);
               CToolchainIdeInfo previousValue = lookupTable.putIfAbsent(l, toolchainInfo);
               // Don't propagate the toolchain twice.
               if (previousValue == null) {
@@ -174,7 +175,7 @@
   public BlazeResolveConfigurationTemporaryBase(
       Project project,
       WorkspacePathResolver workspacePathResolver,
-      Label label,
+      RuleKey ruleKey,
       ImmutableCollection<ExecutionRootPath> cSystemIncludeDirs,
       ImmutableCollection<ExecutionRootPath> cppSystemIncludeDirs,
       ImmutableCollection<ExecutionRootPath> quoteIncludeDirs,
@@ -188,7 +189,7 @@
       ImmutableList<String> cppCompilerFlags) {
     this.workspacePathResolver = workspacePathResolver;
     this.project = project;
-    this.label = label;
+    this.ruleKey = ruleKey;
 
     ImmutableList.Builder<HeadersSearchRoot> cIncludeRootsBuilder = ImmutableList.builder();
     collectHeaderRoots(cIncludeRootsBuilder, cIncludeDirs, true /* isUserHeader */);
@@ -220,7 +221,7 @@
 
   @Override
   public String getDisplayName(boolean shorten) {
-    return label.toString();
+    return ruleKey.toString();
   }
 
   @Nullable
@@ -355,8 +356,8 @@
 
   @Override
   public int hashCode() {
-    // There should only be one configuration per label.
-    return Objects.hash(label);
+    // There should only be one configuration per target.
+    return Objects.hash(ruleKey);
   }
 
   @Override
diff --git a/cpp/src/com/google/idea/blaze/cpp/versioned/v145/BlazeResolveConfiguration.java b/cpp/src/com/google/idea/blaze/cpp/versioned/v145/BlazeResolveConfiguration.java
index 7704f4f..9671707 100644
--- a/cpp/src/com/google/idea/blaze/cpp/versioned/v145/BlazeResolveConfiguration.java
+++ b/cpp/src/com/google/idea/blaze/cpp/versioned/v145/BlazeResolveConfiguration.java
@@ -18,8 +18,8 @@
 import com.google.common.collect.ImmutableCollection;
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableMap;
+import com.google.idea.blaze.base.ideinfo.RuleKey;
 import com.google.idea.blaze.base.model.primitives.ExecutionRootPath;
-import com.google.idea.blaze.base.model.primitives.Label;
 import com.google.idea.blaze.base.sync.workspace.WorkspacePathResolver;
 import com.intellij.openapi.project.Project;
 import com.jetbrains.cidr.execution.CidrBuildTarget;
@@ -36,7 +36,7 @@
   public BlazeResolveConfiguration(
       Project project,
       WorkspacePathResolver workspacePathResolver,
-      Label label,
+      RuleKey ruleKey,
       ImmutableCollection<ExecutionRootPath> cSystemIncludeDirs,
       ImmutableCollection<ExecutionRootPath> cppSystemIncludeDirs,
       ImmutableCollection<ExecutionRootPath> quoteIncludeDirs,
@@ -51,7 +51,7 @@
     super(
         project,
         workspacePathResolver,
-        label,
+        ruleKey,
         cSystemIncludeDirs,
         cppSystemIncludeDirs,
         quoteIncludeDirs,
@@ -71,7 +71,7 @@
     return new CidrBuildTargetWithConfigurations() {
       @Override
       public String getName() {
-        return label.toString();
+        return ruleKey.toString();
       }
 
       @Override
diff --git a/cpp/src/com/google/idea/blaze/cpp/versioned/v162/BlazeResolveConfiguration.java b/cpp/src/com/google/idea/blaze/cpp/versioned/v162/BlazeResolveConfiguration.java
index c5c5e86..8935576 100644
--- a/cpp/src/com/google/idea/blaze/cpp/versioned/v162/BlazeResolveConfiguration.java
+++ b/cpp/src/com/google/idea/blaze/cpp/versioned/v162/BlazeResolveConfiguration.java
@@ -18,20 +18,18 @@
 import com.google.common.collect.ImmutableCollection;
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableMap;
+import com.google.idea.blaze.base.ideinfo.RuleKey;
 import com.google.idea.blaze.base.model.primitives.ExecutionRootPath;
-import com.google.idea.blaze.base.model.primitives.Label;
 import com.google.idea.blaze.base.sync.workspace.WorkspacePathResolver;
 import com.intellij.openapi.project.Project;
-import com.jetbrains.cidr.modulemap.ModuleMapModules;
 import java.io.File;
-import org.jetbrains.annotations.NotNull;
 
 final class BlazeResolveConfiguration extends BlazeResolveConfigurationTemporaryBase {
 
   public BlazeResolveConfiguration(
       Project project,
       WorkspacePathResolver workspacePathResolver,
-      Label label,
+      RuleKey ruleKey,
       ImmutableCollection<ExecutionRootPath> cSystemIncludeDirs,
       ImmutableCollection<ExecutionRootPath> cppSystemIncludeDirs,
       ImmutableCollection<ExecutionRootPath> quoteIncludeDirs,
@@ -46,7 +44,7 @@
     super(
         project,
         workspacePathResolver,
-        label,
+        ruleKey,
         cSystemIncludeDirs,
         cppSystemIncludeDirs,
         quoteIncludeDirs,
@@ -59,10 +57,4 @@
         cCompilerFlags,
         cppCompilerFlags);
   }
-
-  @NotNull
-  @Override
-  public ModuleMapModules getModules() {
-    return ModuleMapModules.Companion.getEMPTY();
-  }
 }
diff --git a/ijwb/.gitignore b/ijwb/.gitignore
deleted file mode 100644
index bec2fed..0000000
--- a/ijwb/.gitignore
+++ /dev/null
@@ -1,8 +0,0 @@
-.idea/workspace.xml
-.idea/tasks.xml
-out
-bazel-bin
-bazel-out
-bazel-genfiles
-bazel-testlogs
-bazel-blaze
diff --git a/ijwb/.bazelproject b/ijwb/ijwb.bazelproject
similarity index 87%
rename from ijwb/.bazelproject
rename to ijwb/ijwb.bazelproject
index f066e1b..e8fc755 100644
--- a/ijwb/.bazelproject
+++ b/ijwb/ijwb.bazelproject
@@ -1,9 +1,8 @@
 directories:
   .
   -aswb
-  -aswb-google3
   -clwb
-  -blaze-cpp
+  -cpp
 
 targets:
   //ijwb:ijwb_bazel
diff --git a/ijwb/src/META-INF/ijwb_bazel.xml b/ijwb/src/META-INF/ijwb_bazel.xml
index 2068ad9..0c72d38 100644
--- a/ijwb/src/META-INF/ijwb_bazel.xml
+++ b/ijwb/src/META-INF/ijwb_bazel.xml
@@ -23,7 +23,7 @@
         <ul>
         <li>Import BUILD files into the IDE.</li>
         <li>BUILD file custom language support.</li>
-        <li>Support for blaze run configurations for certain rule classes.</li>
+        <li>Support for Bazel run configurations for certain rule classes.</li>
         </ul>
 
       Usage instructions at <a href="http://ij.bazel.io">ij.bazel.io</a>
diff --git a/ijwb/src/com/google/idea/blaze/ijwb/android/BlazeAndroidLiteJavaSyncAugmenter.java b/ijwb/src/com/google/idea/blaze/ijwb/android/BlazeAndroidLiteJavaSyncAugmenter.java
index 0d864b8..8aaa2b5 100644
--- a/ijwb/src/com/google/idea/blaze/ijwb/android/BlazeAndroidLiteJavaSyncAugmenter.java
+++ b/ijwb/src/com/google/idea/blaze/ijwb/android/BlazeAndroidLiteJavaSyncAugmenter.java
@@ -43,12 +43,12 @@
     // Add R.java jars
     LibraryArtifact resourceJar = androidRuleIdeInfo.resourceJar;
     if (resourceJar != null) {
-      jars.add(new BlazeJarLibrary(resourceJar, rule.label));
+      jars.add(new BlazeJarLibrary(resourceJar, rule.key));
     }
 
     LibraryArtifact idlJar = androidRuleIdeInfo.idlJar;
     if (idlJar != null) {
-      genJars.add(new BlazeJarLibrary(idlJar, rule.label));
+      genJars.add(new BlazeJarLibrary(idlJar, rule.key));
     }
   }
 }
diff --git a/ijwb/src/com/google/idea/blaze/ijwb/typescript/BlazeTypescriptSyncPlugin.java b/ijwb/src/com/google/idea/blaze/ijwb/typescript/BlazeTypescriptSyncPlugin.java
index 3659a42..726b50e 100644
--- a/ijwb/src/com/google/idea/blaze/ijwb/typescript/BlazeTypescriptSyncPlugin.java
+++ b/ijwb/src/com/google/idea/blaze/ijwb/typescript/BlazeTypescriptSyncPlugin.java
@@ -22,9 +22,9 @@
 import com.google.idea.blaze.base.command.BlazeCommand;
 import com.google.idea.blaze.base.command.BlazeCommandName;
 import com.google.idea.blaze.base.command.BlazeFlags;
+import com.google.idea.blaze.base.ideinfo.RuleMap;
 import com.google.idea.blaze.base.issueparser.IssueOutputLineProcessor;
 import com.google.idea.blaze.base.model.BlazeProjectData;
-import com.google.idea.blaze.base.model.RuleMap;
 import com.google.idea.blaze.base.model.SyncState;
 import com.google.idea.blaze.base.model.primitives.Label;
 import com.google.idea.blaze.base.model.primitives.LanguageClass;
@@ -40,6 +40,7 @@
 import com.google.idea.blaze.base.settings.Blaze;
 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;
@@ -73,6 +74,7 @@
       BlazeRoots blazeRoots,
       @Nullable WorkingSet workingSet,
       WorkspacePathResolver workspacePathResolver,
+      ArtifactLocationDecoder artifactLocationDecoder,
       RuleMap ruleMap,
       SyncState.Builder syncStateBuilder,
       @Nullable SyncState previousSyncState) {
diff --git a/intellij_test/src/com/google/idea/blaze/base/suite/TestAggregator.java b/intellij_test/src/com/google/idea/blaze/base/suite/TestAggregator.java
deleted file mode 100644
index dbae268..0000000
--- a/intellij_test/src/com/google/idea/blaze/base/suite/TestAggregator.java
+++ /dev/null
@@ -1,30 +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.suite;
-
-import java.lang.annotation.ElementType;
-import java.lang.annotation.Retention;
-import java.lang.annotation.RetentionPolicy;
-import java.lang.annotation.Target;
-
-/**
- * Used to denote a class which shouldn't be treated as a test by {@link TestClassFinder}, which
- * searches for all test classes in the classpath matching a pattern.<br>
- * This is useful to prevent infinite loops.
- */
-@Retention(RetentionPolicy.RUNTIME)
-@Target({ElementType.TYPE})
-public @interface TestAggregator {}
diff --git a/intellij_test/src/com/google/idea/blaze/base/suite/TestAll.java b/intellij_test/src/com/google/idea/blaze/base/suite/TestAll.java
deleted file mode 100644
index b40fafe..0000000
--- a/intellij_test/src/com/google/idea/blaze/base/suite/TestAll.java
+++ /dev/null
@@ -1,222 +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.suite;
-
-import com.intellij.TestCaseLoader;
-import com.intellij.openapi.diagnostic.Logger;
-import com.intellij.openapi.util.io.FileUtil;
-import com.intellij.openapi.vfs.VfsUtilCore;
-import com.intellij.testFramework.TeamCityLogger;
-import com.intellij.testFramework.TestLoggerFactory;
-import com.intellij.testFramework.TestRunnerUtil;
-import com.intellij.util.ArrayUtil;
-import java.io.File;
-import java.io.IOException;
-import java.lang.reflect.Method;
-import java.lang.reflect.Modifier;
-import java.net.URL;
-import java.net.URLClassLoader;
-import java.util.Arrays;
-import java.util.Collection;
-import java.util.List;
-import javax.annotation.Nullable;
-import junit.framework.JUnit4TestAdapter;
-import junit.framework.Test;
-import junit.framework.TestCase;
-import junit.framework.TestResult;
-import junit.framework.TestSuite;
-
-/** A cut-down version of {@link com.intellij.TestAll} which supports test classes inside jars. */
-@TestAggregator
-public class TestAll implements Test {
-
-  static {
-    Logger.setFactory(TestLoggerFactory.class);
-  }
-
-  private final TestCaseLoader testCaseLoader;
-
-  public TestAll(String packageRoot) throws Throwable {
-    this(packageRoot, getClassRoots());
-  }
-
-  public TestAll(String packageRoot, String... classRoots)
-      throws IOException, ClassNotFoundException {
-    testCaseLoader = new TestCaseLoader("");
-    fillTestCases(testCaseLoader, packageRoot, classRoots);
-  }
-
-  public static String[] getClassRoots() {
-    final ClassLoader loader = TestAll.class.getClassLoader();
-    if (loader instanceof URLClassLoader) {
-      return getClassRoots(((URLClassLoader) loader).getURLs());
-    }
-    final Class<? extends ClassLoader> loaderClass = loader.getClass();
-    if (loaderClass.getName().equals("com.intellij.util.lang.UrlClassLoader")) {
-      try {
-        final Method declaredMethod = loaderClass.getDeclaredMethod("getBaseUrls");
-        final List<URL> urls = (List<URL>) declaredMethod.invoke(loader);
-        return getClassRoots(urls.toArray(new URL[urls.size()]));
-      } catch (Throwable ignore) {
-        // Do nothing
-      }
-    }
-    return System.getProperty("java.class.path").split(File.pathSeparator);
-  }
-
-  private static String[] getClassRoots(URL[] urls) {
-    return Arrays.stream(urls)
-        .map(VfsUtilCore::convertFromUrl)
-        .map(VfsUtilCore::urlToPath)
-        .toArray(String[]::new);
-  }
-
-  private static boolean isIntellijPlatformJar(String classRoot) {
-    return classRoot.contains("intellij-platform-sdk");
-  }
-
-  public static void fillTestCases(
-      TestCaseLoader testCaseLoader, String packageRoot, String... classRoots) throws IOException {
-    long before = System.currentTimeMillis();
-    for (String classRoot : classRoots) {
-      if (isIntellijPlatformJar(classRoot)) {
-        continue;
-      }
-      int oldCount = testCaseLoader.getClasses().size();
-      File classRootFile = new File(FileUtil.toSystemDependentName(classRoot));
-      Collection<String> classes = TestClassFinder.findTestClasses(classRootFile, packageRoot);
-      testCaseLoader.loadTestCases(classRootFile.getName(), classes);
-      int newCount = testCaseLoader.getClasses().size();
-      if (newCount != oldCount) {
-        System.out.println(
-            "Loaded " + (newCount - oldCount) + " tests from class root " + classRoot);
-      }
-    }
-
-    if (testCaseLoader.getClasses().size() == 1) {
-      testCaseLoader.clearClasses();
-    }
-    long after = System.currentTimeMillis();
-
-    String message =
-        "Number of test classes found: "
-            + testCaseLoader.getClasses().size()
-            + " time to load: "
-            + (after - before) / 1000
-            + "s.";
-    System.out.println(message);
-    log(message);
-  }
-
-  @Override
-  public int countTestCases() {
-    int count = 0;
-    for (Object aClass : testCaseLoader.getClasses()) {
-      Test test = getTest((Class) aClass);
-      if (test != null) {
-        count += test.countTestCases();
-      }
-    }
-    return count;
-  }
-
-  @Override
-  public void run(final TestResult testResult) {
-    List<Class> classes = testCaseLoader.getClasses();
-    for (Class<?> aClass : classes) {
-      runTest(testResult, aClass);
-      if (testResult.shouldStop()) {
-        break;
-      }
-    }
-  }
-
-  private void runTest(final TestResult testResult, Class testCaseClass) {
-    Test test = getTest(testCaseClass);
-    if (test == null) {
-      return;
-    }
-
-    try {
-      test.run(testResult);
-    } catch (Throwable t) {
-      testResult.addError(test, t);
-    }
-  }
-
-  @Nullable
-  private static Test getTest(final Class<?> testCaseClass) {
-    try {
-      if ((testCaseClass.getModifiers() & Modifier.PUBLIC) == 0) {
-        return null;
-      }
-      if (testCaseClass.isAnnotationPresent(TestAggregator.class)) {
-        // prevent infinite loops in 'countTestCases'
-        return null;
-      }
-
-      Method suiteMethod = safeFindMethod(testCaseClass, "suite");
-      if (suiteMethod != null) {
-        return (Test) suiteMethod.invoke(null, (Object[]) ArrayUtil.EMPTY_CLASS_ARRAY);
-      }
-
-      if (TestRunnerUtil.isJUnit4TestClass(testCaseClass)) {
-        return new JUnit4TestAdapter(testCaseClass);
-      }
-
-      final int[] testsCount = {0};
-      TestSuite suite =
-          new TestSuite(testCaseClass) {
-            @Override
-            public void addTest(Test test) {
-              if (!(test instanceof TestCase)) {
-                doAddTest(test);
-              } else {
-                String name = ((TestCase) test).getName();
-                if ("warning".equals(name)) {
-                  return; // Mute TestSuite's "no tests found" warning
-                }
-                doAddTest(test);
-              }
-            }
-
-            private void doAddTest(Test test) {
-              testsCount[0]++;
-              super.addTest(test);
-            }
-          };
-
-      return testsCount[0] > 0 ? suite : null;
-    } catch (Throwable t) {
-      System.err.println("Failed to load test: " + testCaseClass.getName());
-      t.printStackTrace(System.err);
-      return null;
-    }
-  }
-
-  @Nullable
-  private static Method safeFindMethod(Class<?> klass, String name) {
-    try {
-      return klass.getMethod(name);
-    } catch (NoSuchMethodException e) {
-      return null;
-    }
-  }
-
-  private static void log(String message) {
-    TeamCityLogger.info(message);
-  }
-}
diff --git a/intellij_test/src/com/google/idea/blaze/base/suite/TestClassFinder.java b/intellij_test/src/com/google/idea/blaze/base/suite/TestClassFinder.java
deleted file mode 100644
index 40633ed..0000000
--- a/intellij_test/src/com/google/idea/blaze/base/suite/TestClassFinder.java
+++ /dev/null
@@ -1,76 +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.suite;
-
-import com.google.common.collect.Sets;
-import com.intellij.ClassFinder;
-import com.intellij.openapi.util.text.StringUtil;
-import java.io.File;
-import java.io.IOException;
-import java.util.Enumeration;
-import java.util.SortedSet;
-import java.util.zip.ZipEntry;
-import java.util.zip.ZipFile;
-
-/** Finds all valid test classes inside a given directory or jar. */
-@TestAggregator
-public class TestClassFinder {
-
-  private static final String CLASS_EXTENSION = ".class";
-
-  /** Returns all top-level test classes underneath the specified classpath and package roots. */
-  public static SortedSet<String> findTestClasses(File classRootFile, String packageRoot)
-      throws IOException {
-    if (isJar(classRootFile.getPath())) {
-      return findTestClassesInJar(classRootFile, packageRoot);
-    }
-    ClassFinder finder = new ClassFinder(classRootFile, packageRoot, true);
-    return Sets.newTreeSet(finder.getClasses());
-  }
-
-  private static SortedSet<String> findTestClassesInJar(File classPathRoot, String packageRoot)
-      throws IOException {
-    packageRoot = packageRoot.replace('.', File.separatorChar);
-    SortedSet<String> classNames = Sets.newTreeSet();
-    ZipFile zipFile = new ZipFile(classPathRoot.getPath());
-    if (!packageRoot.isEmpty() && zipFile.getEntry(packageRoot) == null) {
-      return Sets.newTreeSet();
-    }
-    Enumeration<? extends ZipEntry> entries = zipFile.entries();
-    while (entries.hasMoreElements()) {
-      String entryName = entries.nextElement().getName();
-      if (entryName.endsWith(CLASS_EXTENSION)
-          && isTopLevelClass(entryName)
-          && entryName.startsWith(packageRoot)) {
-        classNames.add(getClassName(entryName));
-      }
-    }
-    return classNames;
-  }
-
-  private static boolean isJar(String filePath) {
-    return filePath.endsWith(".jar");
-  }
-
-  private static boolean isTopLevelClass(String fileName) {
-    return fileName.indexOf('$') < 0;
-  }
-
-  /** Given the absolute path of a class file, return the class name. */
-  private static String getClassName(String className) {
-    return StringUtil.trimEnd(className, CLASS_EXTENSION).replace(File.separatorChar, '.');
-  }
-}
diff --git a/intellij_test/src/com/google/idea/blaze/base/suite/TestSuiteBuilder.java b/intellij_test/src/com/google/idea/blaze/base/suite/TestSuiteBuilder.java
deleted file mode 100644
index 9d918a1..0000000
--- a/intellij_test/src/com/google/idea/blaze/base/suite/TestSuiteBuilder.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.base.suite;
-
-import com.google.common.base.Strings;
-import com.google.idea.blaze.base.BlazeTestSystemProperties;
-import junit.framework.Test;
-import junit.framework.TestSuite;
-import org.junit.runner.RunWith;
-import org.junit.runners.AllTests;
-
-/** Simple JUnit3 style test suite builder. */
-@TestAggregator
-@RunWith(AllTests.class)
-public class TestSuiteBuilder {
-  public static Test suite() throws Throwable {
-
-    BlazeTestSystemProperties.configureSystemProperties();
-
-    String packageRoot = System.getProperty("idea.test.package.root");
-    packageRoot = Strings.nullToEmpty(packageRoot);
-
-    TestSuite suite = new TestSuite();
-    suite.addTest(new TestAll(packageRoot));
-    return suite;
-  }
-}
diff --git a/intellij_test/test_defs.bzl b/intellij_test/test_defs.bzl
deleted file mode 100644
index 52205c4..0000000
--- a/intellij_test/test_defs.bzl
+++ /dev/null
@@ -1,114 +0,0 @@
-"""Custom rule for creating IntelliJ plugin tests.
-"""
-
-def intellij_unit_test_suite(name, srcs, test_package_root, **kwargs):
-  """Creates a java_test rule comprising all valid test classes in the specified srcs.
-
-  Args:
-    name: name of this rule.
-    srcs: the test classes.
-    test_package_root: only tests under this package root will be run.
-    **kwargs: Any other args to be passed to the java_test.
-  """
-  test_srcs = [test for test in srcs if test.endswith("Test.java")]
-  test_classes = [_get_test_class(test_src, test_package_root) for test_src in test_srcs]
-  suite_class_name = name + "TestSuite"
-  suite_class = test_package_root + "." + suite_class_name
-  _generate_test_suite(
-      name = suite_class_name,
-      test_package_root = test_package_root,
-      test_classes = test_classes,
-  )
-  native.java_test(
-      name = name,
-      srcs = srcs + [suite_class_name],
-      test_class = suite_class,
-      **kwargs)
-
-def _generate_test_suite(name, test_package_root, test_classes):
-  """Generates a JUnit test suite pulling in all the referenced classes."""
-  lines = []
-  lines.append("package %s;" % test_package_root)
-  lines.append("")
-  lines.append("import org.junit.runner.RunWith;")
-  lines.append("import org.junit.runners.Suite;")
-  lines.append("")
-  for test_class in test_classes:
-    lines.append("import %s;" % test_class)
-  lines.append("")
-  lines.append("@RunWith(Suite.class)")
-  lines.append("@Suite.SuiteClasses({")
-  for test_class in test_classes:
-    lines.append("    %s.class," % test_class.split(".")[-1])
-  lines.append("})")
-  lines.append("class %s {}" % name)
-
-  contents = "\\n".join(lines)
-  native.genrule(
-      name = name,
-      cmd = "printf '%s' > $@" % contents,
-      outs = [name + ".java"],
-  )
-
-
-def _get_test_class(test_src, test_package_root):
-  """Returns the test class of the source relative to the given root."""
-  temp = test_src[:-5]
-  temp = temp.replace("/", ".")
-  i = temp.rfind(test_package_root)
-  if i < 0:
-    fail("Test source '%s' not under package root '%s'" % (test_src, test_package_root))
-  test_class = temp[i:]
-  return test_class
-
-def intellij_integration_test_suite(
-    name,
-    srcs,
-    test_package_root,
-    deps,
-    runtime_deps = [],
-    platform_prefix="Idea",
-    required_plugins=None,
-    **kwargs):
-  """Creates a java_test rule comprising all valid test classes in the specified srcs.
-
-  Args:
-    name: name of this rule.
-    srcs: the test classes.
-    test_package_root: only tests under this package root will be run.
-    deps: the required deps.
-    runtime_deps: the required runtime_deps.
-    platform_prefix: Specifies the JetBrains product these tests are run against. Examples are
-        'Idea' (IJ CE), 'idea' (IJ UE), 'CLion', 'AndroidStudio'. See
-        com.intellij.util.PlatformUtils for other options.
-    required_plugins: optional comma-separated list of plugin IDs. Integration tests will fail if
-        these plugins aren't loaded at runtime.
-    **kwargs: Any other args to be passed to the java_test.
-  """
-
-  runtime_deps = list(runtime_deps)
-  runtime_deps.extend([
-      "//intellij_test:lib",
-      "//intellij_platform_sdk:bundled_plugins",
-      "//third_party:jdk8_tools",
-  ])
-
-  jvm_flags = [
-      "-Didea.classpath.index.enabled=false",
-      "-Djava.awt.headless=true",
-      "-Didea.platform.prefix=" + platform_prefix,
-      "-Didea.test.package.root=" + test_package_root,
-  ]
-
-  if required_plugins:
-    jvm_flags.append("-Didea.required.plugins.id=" + required_plugins)
-
-  native.java_test(
-      name = name,
-      srcs = srcs,
-      deps = deps,
-      runtime_deps = runtime_deps,
-      size = "medium",
-      jvm_flags = jvm_flags,
-      test_class = "com.google.idea.blaze.base.suite.TestSuiteBuilder",
-      **kwargs)
diff --git a/java/BUILD b/java/BUILD
index 4ba94eb..2655b20 100644
--- a/java/BUILD
+++ b/java/BUILD
@@ -52,7 +52,7 @@
 )
 
 load(
-    "//intellij_test:test_defs.bzl",
+    "//testing:test_defs.bzl",
     "intellij_integration_test_suite",
     "intellij_unit_test_suite",
 )
@@ -89,5 +89,6 @@
         "//base:unit_test_utils",
         "//intellij_platform_sdk:plugin_api_for_tests",
         "@jsr305_annotations//jar",
+        "@junit//jar",
     ],
 )
diff --git a/java/src/META-INF/blaze-java.xml b/java/src/META-INF/blaze-java.xml
index 321e5e4..b2ae955 100644
--- a/java/src/META-INF/blaze-java.xml
+++ b/java/src/META-INF/blaze-java.xml
@@ -37,6 +37,15 @@
       <add-to-group group-id="Blaze.ProjectViewPopupMenu"/>
     </action>
 
+    <group>
+      <action class="com.google.idea.blaze.java.libraries.DetachAllSourceJarsAction"
+        id="Blaze.DetachAllSourceJars"
+        text="Detach All Blaze Source Jars">
+      </action>
+      <separator/>
+      <add-to-group group-id="Blaze.MainMenuActionGroup" relative-to-action="Blaze.EditProjectView" anchor="before"/>
+    </group>
+
     <!-- IntelliJ specific actions -->
 
     <action id="Blaze.ImportProject2" class="com.google.idea.blaze.java.wizard2.BlazeImportProjectAction" icon="BlazeIcons.Blaze">
@@ -52,11 +61,12 @@
     <SyncPlugin implementation="com.google.idea.blaze.java.sync.BlazeJavaSyncPlugin"/>
     <PsiFileProvider implementation="com.google.idea.blaze.java.psi.JavaPsiFileProvider" />
     <BlazeCommandRunConfigurationHandlerProvider implementation="com.google.idea.blaze.java.run.BlazeJavaRunConfigurationHandlerProvider"/>
-    <RuleConfigurationFactory implementation="com.google.idea.blaze.java.run.BlazeJavaRuleConfigurationFactory"/>
-    <RuleConfigurationFactory implementation="com.google.idea.blaze.java.run.BlazeJavaTestRuleConfigurationFactory"/>
+    <RunConfigurationFactory implementation="com.google.idea.blaze.java.run.BlazeJavaRunConfigurationFactory"/>
+    <RunConfigurationFactory implementation="com.google.idea.blaze.java.run.BlazeJavaTestRunConfigurationFactory"/>
     <BlazeUserSettingsContributor implementation="com.google.idea.blaze.java.settings.BlazeJavaUserSettingsContributor$BlazeJavaUserSettingsProvider"/>
     <FileCache implementation="com.google.idea.blaze.java.libraries.JarCache$FileCacheAdapter"/>
     <PrefetchFileSource implementation="com.google.idea.blaze.java.sync.JavaPrefetchFileSource"/>
+    <SyncListener implementation="com.google.idea.blaze.java.syncstatus.SyncStatusHelper$UpdateSyncStatusMap"/>
   </extensions>
 
   <extensions defaultExtensionNs="com.intellij">
@@ -87,6 +97,7 @@
     <attachSourcesProvider implementation="com.google.idea.blaze.java.libraries.AddLibraryRuleDirectoryToProjectViewAttachSourcesProvider"/>
     <attachSourcesProvider implementation="com.google.idea.blaze.java.libraries.BlazeAttachSourceProvider"/>
     <applicationService serviceImplementation="com.google.idea.blaze.java.settings.BlazeJavaUserSettings"/>
+    <projectService serviceImplementation="com.google.idea.blaze.java.syncstatus.SyncStatusHelper"/>
   </extensions>
 
   <extensionPoints>
diff --git a/java/src/com/google/idea/blaze/java/libraries/AddLibraryRuleDirectoryToProjectViewAction.java b/java/src/com/google/idea/blaze/java/libraries/AddLibraryRuleDirectoryToProjectViewAction.java
index ac34691..1bf26e7 100644
--- a/java/src/com/google/idea/blaze/java/libraries/AddLibraryRuleDirectoryToProjectViewAction.java
+++ b/java/src/com/google/idea/blaze/java/libraries/AddLibraryRuleDirectoryToProjectViewAction.java
@@ -19,9 +19,9 @@
 import com.google.common.collect.Sets;
 import com.google.idea.blaze.base.actions.BlazeAction;
 import com.google.idea.blaze.base.ideinfo.RuleIdeInfo;
+import com.google.idea.blaze.base.ideinfo.RuleKey;
 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.WorkspacePath;
 import com.google.idea.blaze.base.projectview.ProjectViewEdit;
 import com.google.idea.blaze.base.projectview.ProjectViewManager;
@@ -78,20 +78,20 @@
 
   @Nullable
   static WorkspacePath getDirectoryToAddForLibrary(Project project, Library library) {
-    BlazeJarLibrary blazeLibrary =
-        LibraryActionHelper.findLibraryFromIntellijLibrary(project, library);
-    if (blazeLibrary == null) {
-      return null;
-    }
-    Label originatingRule = blazeLibrary.originatingRule;
-    if (originatingRule == null) {
-      return null;
-    }
     BlazeProjectData blazeProjectData =
         BlazeProjectDataManager.getInstance(project).getBlazeProjectData();
     if (blazeProjectData == null) {
       return null;
     }
+    BlazeJarLibrary blazeLibrary =
+        LibraryActionHelper.findLibraryFromIntellijLibrary(project, blazeProjectData, library);
+    if (blazeLibrary == null) {
+      return null;
+    }
+    RuleKey originatingRule = blazeLibrary.originatingRule;
+    if (originatingRule == null) {
+      return null;
+    }
     RuleIdeInfo rule = blazeProjectData.ruleMap.get(originatingRule);
     if (rule == null) {
       return null;
diff --git a/java/src/com/google/idea/blaze/java/libraries/AttachSourceJarAction.java b/java/src/com/google/idea/blaze/java/libraries/AttachSourceJarAction.java
index ca216ac..d6daa4f 100644
--- a/java/src/com/google/idea/blaze/java/libraries/AttachSourceJarAction.java
+++ b/java/src/com/google/idea/blaze/java/libraries/AttachSourceJarAction.java
@@ -17,6 +17,8 @@
 
 import com.google.idea.blaze.base.actions.BlazeAction;
 import com.google.idea.blaze.base.ideinfo.LibraryArtifact;
+import com.google.idea.blaze.base.model.BlazeProjectData;
+import com.google.idea.blaze.base.sync.data.BlazeProjectDataManager;
 import com.google.idea.blaze.java.sync.model.BlazeJarLibrary;
 import com.google.idea.blaze.java.sync.projectstructure.LibraryEditor;
 import com.intellij.CommonBundle;
@@ -35,10 +37,15 @@
   public void actionPerformed(AnActionEvent e) {
     Project project = e.getProject();
     assert project != null;
+    BlazeProjectData blazeProjectData =
+        BlazeProjectDataManager.getInstance(project).getBlazeProjectData();
+    if (blazeProjectData == null) {
+      return;
+    }
     Library library = LibraryActionHelper.findLibraryForAction(e);
     if (library != null) {
       BlazeJarLibrary blazeLibrary =
-          LibraryActionHelper.findLibraryFromIntellijLibrary(project, library);
+          LibraryActionHelper.findLibraryFromIntellijLibrary(project, blazeProjectData, library);
       if (blazeLibrary == null) {
         Messages.showErrorDialog(
             project, "Could not find this library in the project.", CommonBundle.getErrorTitle());
@@ -58,7 +65,12 @@
               () -> {
                 LibraryTable libraryTable = ProjectLibraryTable.getInstance(project);
                 LibraryTable.ModifiableModel libraryTableModel = libraryTable.getModifiableModel();
-                LibraryEditor.updateLibrary(project, libraryTable, libraryTableModel, blazeLibrary);
+                LibraryEditor.updateLibrary(
+                    project,
+                    blazeProjectData.artifactLocationDecoder,
+                    libraryTable,
+                    libraryTableModel,
+                    blazeLibrary);
                 libraryTableModel.commit();
               });
     }
@@ -72,16 +84,21 @@
     boolean enabled = false;
     Project project = e.getProject();
     if (project != null) {
-      Library library = LibraryActionHelper.findLibraryForAction(e);
-      if (library != null) {
-        visible = true;
+      BlazeProjectData blazeProjectData =
+          BlazeProjectDataManager.getInstance(project).getBlazeProjectData();
+      if (blazeProjectData != null) {
+        Library library = LibraryActionHelper.findLibraryForAction(e);
+        if (library != null) {
+          visible = true;
 
-        BlazeJarLibrary blazeLibrary =
-            LibraryActionHelper.findLibraryFromIntellijLibrary(e.getProject(), library);
-        if (blazeLibrary != null && blazeLibrary.libraryArtifact.sourceJar != null) {
-          enabled = true;
-          if (SourceJarManager.getInstance(project).hasSourceJarAttached(blazeLibrary.key)) {
-            text = "Detach Source Jar";
+          BlazeJarLibrary blazeLibrary =
+              LibraryActionHelper.findLibraryFromIntellijLibrary(
+                  e.getProject(), blazeProjectData, library);
+          if (blazeLibrary != null && blazeLibrary.libraryArtifact.sourceJar != null) {
+            enabled = true;
+            if (SourceJarManager.getInstance(project).hasSourceJarAttached(blazeLibrary.key)) {
+              text = "Detach Source Jar";
+            }
           }
         }
       }
diff --git a/java/src/com/google/idea/blaze/java/libraries/BlazeAttachSourceProvider.java b/java/src/com/google/idea/blaze/java/libraries/BlazeAttachSourceProvider.java
index b70ba2f..04e0d8c 100644
--- a/java/src/com/google/idea/blaze/java/libraries/BlazeAttachSourceProvider.java
+++ b/java/src/com/google/idea/blaze/java/libraries/BlazeAttachSourceProvider.java
@@ -28,7 +28,6 @@
 import com.google.idea.blaze.java.sync.projectstructure.LibraryEditor;
 import com.intellij.codeInsight.AttachSourcesProvider;
 import com.intellij.openapi.application.ApplicationManager;
-import com.intellij.openapi.diagnostic.Logger;
 import com.intellij.openapi.project.Project;
 import com.intellij.openapi.roots.LibraryOrderEntry;
 import com.intellij.openapi.roots.impl.libraries.ProjectLibraryTable;
@@ -43,8 +42,6 @@
 
 /** @author Sergey Evdokimov */
 public class BlazeAttachSourceProvider implements AttachSourcesProvider {
-  private static final Logger LOG = Logger.getInstance(BlazeAttachSourceProvider.class);
-
   @NotNull
   @Override
   public Collection<AttachSourcesAction> getActions(
@@ -67,7 +64,7 @@
         continue;
       }
       BlazeJarLibrary blazeLibrary =
-          LibraryActionHelper.findLibraryFromIntellijLibrary(project, library);
+          LibraryActionHelper.findLibraryFromIntellijLibrary(project, blazeProjectData, library);
       if (blazeLibrary == null) {
         continue;
       }
@@ -90,7 +87,7 @@
     if (BlazeJavaUserSettings.getInstance().getAttachSourcesOnDemand()) {
       UIUtil.invokeLaterIfNeeded(
           () -> {
-            attachSources(project, librariesToAttachSourceTo);
+            attachSources(project, blazeProjectData, librariesToAttachSourceTo);
           });
       return ImmutableList.of();
     }
@@ -109,13 +106,16 @@
 
           @Override
           public ActionCallback perform(List<LibraryOrderEntry> orderEntriesContainingFile) {
-            attachSources(project, librariesToAttachSourceTo);
+            attachSources(project, blazeProjectData, librariesToAttachSourceTo);
             return ActionCallback.DONE;
           }
         });
   }
 
-  static void attachSources(Project project, Collection<BlazeLibrary> librariesToAttachSourceTo) {
+  static void attachSources(
+      Project project,
+      BlazeProjectData blazeProjectData,
+      Collection<BlazeLibrary> librariesToAttachSourceTo) {
     ApplicationManager.getApplication()
         .runWriteAction(
             () -> {
@@ -128,7 +128,12 @@
                 }
                 SourceJarManager.getInstance(project)
                     .setHasSourceJarAttached(blazeLibrary.key, true);
-                LibraryEditor.updateLibrary(project, libraryTable, libraryTableModel, blazeLibrary);
+                LibraryEditor.updateLibrary(
+                    project,
+                    blazeProjectData.artifactLocationDecoder,
+                    libraryTable,
+                    libraryTableModel,
+                    blazeLibrary);
               }
               libraryTableModel.commit();
             });
diff --git a/java/src/com/google/idea/blaze/java/libraries/DetachAllSourceJarsAction.java b/java/src/com/google/idea/blaze/java/libraries/DetachAllSourceJarsAction.java
new file mode 100644
index 0000000..f30518a
--- /dev/null
+++ b/java/src/com/google/idea/blaze/java/libraries/DetachAllSourceJarsAction.java
@@ -0,0 +1,77 @@
+/*
+ * Copyright 2016 The Bazel Authors. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.idea.blaze.java.libraries;
+
+import com.google.common.collect.Lists;
+import com.google.idea.blaze.base.actions.BlazeAction;
+import com.google.idea.blaze.base.model.BlazeProjectData;
+import com.google.idea.blaze.base.sync.data.BlazeProjectDataManager;
+import com.google.idea.blaze.java.sync.model.BlazeJarLibrary;
+import com.google.idea.blaze.java.sync.model.LibraryKey;
+import com.google.idea.blaze.java.sync.projectstructure.LibraryEditor;
+import com.intellij.openapi.actionSystem.AnActionEvent;
+import com.intellij.openapi.application.ApplicationManager;
+import com.intellij.openapi.project.Project;
+import com.intellij.openapi.roots.impl.libraries.ProjectLibraryTable;
+import com.intellij.openapi.roots.libraries.Library;
+import com.intellij.openapi.roots.libraries.LibraryTable;
+import java.util.List;
+
+class DetachAllSourceJarsAction extends BlazeAction {
+  @Override
+  public void actionPerformed(AnActionEvent e) {
+    Project project = e.getProject();
+    assert project != null;
+
+    BlazeProjectData blazeProjectData =
+        BlazeProjectDataManager.getInstance(project).getBlazeProjectData();
+    if (blazeProjectData == null) {
+      return;
+    }
+
+    List<Library> librariesToDetach = Lists.newArrayList();
+    SourceJarManager sourceJarManager = SourceJarManager.getInstance(project);
+    for (Library library : ProjectLibraryTable.getInstance(project).getLibraries()) {
+      LibraryKey libraryKey = LibraryKey.fromIntelliJLibrary(library);
+      if (sourceJarManager.hasSourceJarAttached(libraryKey)) {
+        sourceJarManager.setHasSourceJarAttached(libraryKey, false);
+        librariesToDetach.add(library);
+      }
+    }
+
+    ApplicationManager.getApplication()
+        .runWriteAction(
+            () -> {
+              LibraryTable libraryTable = ProjectLibraryTable.getInstance(project);
+              LibraryTable.ModifiableModel libraryTableModel = libraryTable.getModifiableModel();
+              for (Library library : librariesToDetach) {
+                BlazeJarLibrary blazeLibrary =
+                    LibraryActionHelper.findLibraryFromIntellijLibrary(
+                        e.getProject(), blazeProjectData, library);
+                if (blazeLibrary == null) {
+                  continue;
+                }
+                LibraryEditor.updateLibrary(
+                    project,
+                    blazeProjectData.artifactLocationDecoder,
+                    libraryTable,
+                    libraryTableModel,
+                    blazeLibrary);
+              }
+              libraryTableModel.commit();
+            });
+  }
+}
diff --git a/java/src/com/google/idea/blaze/java/libraries/ExcludeLibraryAction.java b/java/src/com/google/idea/blaze/java/libraries/ExcludeLibraryAction.java
index 8d2776e..53d4ae8 100644
--- a/java/src/com/google/idea/blaze/java/libraries/ExcludeLibraryAction.java
+++ b/java/src/com/google/idea/blaze/java/libraries/ExcludeLibraryAction.java
@@ -17,12 +17,14 @@
 
 import com.google.idea.blaze.base.actions.BlazeAction;
 import com.google.idea.blaze.base.ideinfo.LibraryArtifact;
+import com.google.idea.blaze.base.model.BlazeProjectData;
 import com.google.idea.blaze.base.projectview.ProjectViewEdit;
 import com.google.idea.blaze.base.projectview.section.Glob;
 import com.google.idea.blaze.base.projectview.section.ListSection;
 import com.google.idea.blaze.base.settings.BlazeUserSettings;
 import com.google.idea.blaze.base.sync.BlazeSyncManager;
 import com.google.idea.blaze.base.sync.BlazeSyncParams;
+import com.google.idea.blaze.base.sync.data.BlazeProjectDataManager;
 import com.google.idea.blaze.java.projectview.ExcludeLibrarySection;
 import com.google.idea.blaze.java.sync.model.BlazeJarLibrary;
 import com.intellij.CommonBundle;
@@ -38,45 +40,51 @@
   public void actionPerformed(AnActionEvent e) {
     Project project = e.getProject();
     assert project != null;
-    Library library = LibraryActionHelper.findLibraryForAction(e);
-    if (library != null) {
-      BlazeJarLibrary blazeLibrary =
-          LibraryActionHelper.findLibraryFromIntellijLibrary(project, library);
-      if (blazeLibrary == null) {
-        Messages.showErrorDialog(
-            project, "Could not find this library in the project.", CommonBundle.getErrorTitle());
-        return;
-      }
-
-      final LibraryArtifact libraryArtifact = blazeLibrary.libraryArtifact;
-      final String path = libraryArtifact.jarForIntellijLibrary().getRelativePath();
-
-      ProjectViewEdit edit =
-          ProjectViewEdit.editLocalProjectView(
-              project,
-              builder -> {
-                ListSection<Glob> existingSection = builder.getLast(ExcludeLibrarySection.KEY);
-                builder.replace(
-                    existingSection,
-                    ListSection.update(ExcludeLibrarySection.KEY, existingSection)
-                        .add(new Glob(path)));
-                return true;
-              });
-      if (edit == null) {
-        Messages.showErrorDialog(
-            "Could not modify project view. Check for errors in your project view and try again",
-            "Error");
-        return;
-      }
-      edit.apply();
-
-      BlazeSyncManager.getInstance(project)
-          .requestProjectSync(
-              new BlazeSyncParams.Builder("Sync", BlazeSyncParams.SyncMode.INCREMENTAL)
-                  .addProjectViewTargets(true)
-                  .addWorkingSet(BlazeUserSettings.getInstance().getExpandSyncToWorkingSet())
-                  .build());
+    BlazeProjectData blazeProjectData =
+        BlazeProjectDataManager.getInstance(project).getBlazeProjectData();
+    if (blazeProjectData == null) {
+      return;
     }
+    Library library = LibraryActionHelper.findLibraryForAction(e);
+    if (library == null) {
+      return;
+    }
+    BlazeJarLibrary blazeLibrary =
+        LibraryActionHelper.findLibraryFromIntellijLibrary(project, blazeProjectData, library);
+    if (blazeLibrary == null) {
+      Messages.showErrorDialog(
+          project, "Could not find this library in the project.", CommonBundle.getErrorTitle());
+      return;
+    }
+
+    final LibraryArtifact libraryArtifact = blazeLibrary.libraryArtifact;
+    final String path = libraryArtifact.jarForIntellijLibrary().getRelativePath();
+
+    ProjectViewEdit edit =
+        ProjectViewEdit.editLocalProjectView(
+            project,
+            builder -> {
+              ListSection<Glob> existingSection = builder.getLast(ExcludeLibrarySection.KEY);
+              builder.replace(
+                  existingSection,
+                  ListSection.update(ExcludeLibrarySection.KEY, existingSection)
+                      .add(new Glob(path)));
+              return true;
+            });
+    if (edit == null) {
+      Messages.showErrorDialog(
+          "Could not modify project view. Check for errors in your project view and try again",
+          "Error");
+      return;
+    }
+    edit.apply();
+
+    BlazeSyncManager.getInstance(project)
+        .requestProjectSync(
+            new BlazeSyncParams.Builder("Sync", BlazeSyncParams.SyncMode.INCREMENTAL)
+                .addProjectViewTargets(true)
+                .addWorkingSet(BlazeUserSettings.getInstance().getExpandSyncToWorkingSet())
+                .build());
   }
 
   @Override
diff --git a/java/src/com/google/idea/blaze/java/libraries/JarCache.java b/java/src/com/google/idea/blaze/java/libraries/JarCache.java
index 33b5cad..b0a8ec1 100644
--- a/java/src/com/google/idea/blaze/java/libraries/JarCache.java
+++ b/java/src/com/google/idea/blaze/java/libraries/JarCache.java
@@ -34,6 +34,7 @@
 import com.google.idea.blaze.base.settings.BlazeImportSettingsManager;
 import com.google.idea.blaze.base.sync.BlazeSyncParams;
 import com.google.idea.blaze.base.sync.data.BlazeDataStorage;
+import com.google.idea.blaze.base.sync.workspace.ArtifactLocationDecoder;
 import com.google.idea.blaze.java.settings.BlazeJavaUserSettings;
 import com.google.idea.blaze.java.sync.BlazeLibraryCollector;
 import com.google.idea.blaze.java.sync.model.BlazeJarLibrary;
@@ -101,15 +102,17 @@
             .map(library -> (BlazeJarLibrary) library)
             .collect(Collectors.toList());
 
+    ArtifactLocationDecoder artifactLocationDecoder = projectData.artifactLocationDecoder;
     BiMap<File, String> sourceFileToCacheKey = HashBiMap.create(jarLibraries.size());
     for (BlazeJarLibrary library : jarLibraries) {
-      File jarFile = library.libraryArtifact.jarForIntellijLibrary().getFile();
+      File jarFile =
+          artifactLocationDecoder.decode(library.libraryArtifact.jarForIntellijLibrary());
       sourceFileToCacheKey.put(jarFile, cacheKeyForJar(jarFile));
 
       boolean attachSourceJar =
           attachAllSourceJars || sourceJarManager.hasSourceJarAttached(library.key);
       if (attachSourceJar && library.libraryArtifact.sourceJar != null) {
-        File srcJarFile = library.libraryArtifact.sourceJar.getFile();
+        File srcJarFile = artifactLocationDecoder.decode(library.libraryArtifact.sourceJar);
         sourceFileToCacheKey.put(srcJarFile, cacheKeyForSourceJar(srcJarFile));
       }
     }
@@ -262,8 +265,8 @@
   }
 
   /** Gets the cached file for a jar. If it doesn't exist, we return the file from the library. */
-  public File getCachedJar(BlazeJarLibrary library) {
-    File file = library.libraryArtifact.jarForIntellijLibrary().getFile();
+  public File getCachedJar(ArtifactLocationDecoder decoder, BlazeJarLibrary library) {
+    File file = decoder.decode(library.libraryArtifact.jarForIntellijLibrary());
     if (!enabled || sourceFileToCacheKey == null) {
       return file;
     }
@@ -276,11 +279,11 @@
 
   /** Gets the cached file for a source jar. */
   @Nullable
-  public File getCachedSourceJar(BlazeJarLibrary library) {
+  public File getCachedSourceJar(ArtifactLocationDecoder decoder, BlazeJarLibrary library) {
     if (library.libraryArtifact.sourceJar == null) {
       return null;
     }
-    File file = library.libraryArtifact.sourceJar.getFile();
+    File file = decoder.decode(library.libraryArtifact.sourceJar);
     if (!enabled || sourceFileToCacheKey == null) {
       return file;
     }
diff --git a/java/src/com/google/idea/blaze/java/libraries/LibraryActionHelper.java b/java/src/com/google/idea/blaze/java/libraries/LibraryActionHelper.java
index 3b3132c..7e7db55 100644
--- a/java/src/com/google/idea/blaze/java/libraries/LibraryActionHelper.java
+++ b/java/src/com/google/idea/blaze/java/libraries/LibraryActionHelper.java
@@ -16,7 +16,6 @@
 package com.google.idea.blaze.java.libraries;
 
 import com.google.idea.blaze.base.model.BlazeProjectData;
-import com.google.idea.blaze.base.sync.data.BlazeProjectDataManager;
 import com.google.idea.blaze.java.sync.model.BlazeJarLibrary;
 import com.google.idea.blaze.java.sync.model.BlazeJavaSyncData;
 import com.google.idea.blaze.java.sync.model.BlazeLibrary;
@@ -37,14 +36,10 @@
 
 class LibraryActionHelper {
 
-  static BlazeJarLibrary findLibraryFromIntellijLibrary(Project project, Library library) {
+  static BlazeJarLibrary findLibraryFromIntellijLibrary(
+      Project project, BlazeProjectData blazeProjectData, Library library) {
     LibraryKey libraryKey = LibraryKey.fromIntelliJLibrary(library);
-    BlazeProjectData projectData =
-        BlazeProjectDataManager.getInstance(project).getBlazeProjectData();
-    if (projectData == null) {
-      return null;
-    }
-    BlazeJavaSyncData syncData = projectData.syncState.get(BlazeJavaSyncData.class);
+    BlazeJavaSyncData syncData = blazeProjectData.syncState.get(BlazeJavaSyncData.class);
     if (syncData == null) {
       Messages.showErrorDialog(project, "Project isn't synced. Please resync project.", "Error");
       return null;
diff --git a/java/src/com/google/idea/blaze/java/run/BlazeJavaDebuggerRunner.java b/java/src/com/google/idea/blaze/java/run/BlazeJavaDebuggerRunner.java
index 55f9900..001f26e 100644
--- a/java/src/com/google/idea/blaze/java/run/BlazeJavaDebuggerRunner.java
+++ b/java/src/com/google/idea/blaze/java/run/BlazeJavaDebuggerRunner.java
@@ -15,7 +15,7 @@
  */
 package com.google.idea.blaze.java.run;
 
-import com.google.idea.blaze.base.ideinfo.RuleIdeInfo;
+import com.google.idea.blaze.base.model.primitives.Kind;
 import com.google.idea.blaze.base.run.BlazeCommandRunConfiguration;
 import com.intellij.debugger.impl.GenericDebuggerRunner;
 import com.intellij.execution.ExecutionException;
@@ -43,8 +43,8 @@
     if (executorId.equals(DefaultDebugExecutor.EXECUTOR_ID)
         && profile instanceof BlazeCommandRunConfiguration) {
       BlazeCommandRunConfiguration configuration = (BlazeCommandRunConfiguration) profile;
-      RuleIdeInfo rule = configuration.getRuleForTarget();
-      return rule != null && BlazeJavaRunConfigurationHandlerProvider.supportsKind(rule.kind);
+      Kind kind = configuration.getKindForTarget();
+      return kind != null && BlazeJavaRunConfigurationHandlerProvider.supportsKind(kind);
     }
     return false;
   }
diff --git a/java/src/com/google/idea/blaze/java/run/BlazeJavaRuleConfigurationFactory.java b/java/src/com/google/idea/blaze/java/run/BlazeJavaRunConfigurationFactory.java
similarity index 61%
rename from java/src/com/google/idea/blaze/java/run/BlazeJavaRuleConfigurationFactory.java
rename to java/src/com/google/idea/blaze/java/run/BlazeJavaRunConfigurationFactory.java
index 2ed1ca8..eb2c0d2 100644
--- a/java/src/com/google/idea/blaze/java/run/BlazeJavaRuleConfigurationFactory.java
+++ b/java/src/com/google/idea/blaze/java/run/BlazeJavaRunConfigurationFactory.java
@@ -17,21 +17,24 @@
 
 import com.google.idea.blaze.base.command.BlazeCommandName;
 import com.google.idea.blaze.base.ideinfo.RuleIdeInfo;
+import com.google.idea.blaze.base.ideinfo.RuleKey;
+import com.google.idea.blaze.base.model.BlazeProjectData;
 import com.google.idea.blaze.base.model.primitives.Kind;
+import com.google.idea.blaze.base.model.primitives.Label;
 import com.google.idea.blaze.base.run.BlazeCommandRunConfiguration;
 import com.google.idea.blaze.base.run.BlazeCommandRunConfigurationType;
-import com.google.idea.blaze.base.run.BlazeRuleConfigurationFactory;
-import com.google.idea.blaze.base.run.confighandler.BlazeCommandGenericRunConfigurationHandler;
-import com.google.idea.blaze.base.sync.projectview.WorkspaceLanguageSettings;
+import com.google.idea.blaze.base.run.BlazeRunConfigurationFactory;
+import com.google.idea.blaze.base.run.state.BlazeCommandRunConfigurationCommonState;
 import com.intellij.execution.configurations.ConfigurationFactory;
 import com.intellij.execution.configurations.RunConfiguration;
+import com.intellij.openapi.project.Project;
 
 /** Creates run configurations for java_binary. */
-public class BlazeJavaRuleConfigurationFactory extends BlazeRuleConfigurationFactory {
+public class BlazeJavaRunConfigurationFactory extends BlazeRunConfigurationFactory {
   @Override
-  public boolean handlesRule(
-      WorkspaceLanguageSettings workspaceLanguageSettings, RuleIdeInfo rule) {
-    return rule.kind == Kind.JAVA_BINARY;
+  public boolean handlesTarget(Project project, BlazeProjectData blazeProjectData, Label target) {
+    RuleIdeInfo rule = blazeProjectData.ruleMap.get(RuleKey.forPlainTarget(target));
+    return rule != null && rule.kind == Kind.JAVA_BINARY;
   }
 
   @Override
@@ -40,14 +43,15 @@
   }
 
   @Override
-  public void setupConfiguration(RunConfiguration configuration, RuleIdeInfo rule) {
+  public void setupConfiguration(RunConfiguration configuration, Label target) {
     final BlazeCommandRunConfiguration blazeConfig = (BlazeCommandRunConfiguration) configuration;
-    blazeConfig.setTarget(rule.label);
+    blazeConfig.setTarget(target);
 
-    BlazeCommandGenericRunConfigurationHandler handler =
-        (BlazeCommandGenericRunConfigurationHandler) blazeConfig.getHandler();
-    handler.setCommand(BlazeCommandName.RUN);
-
+    BlazeCommandRunConfigurationCommonState state =
+        blazeConfig.getHandlerStateIfType(BlazeCommandRunConfigurationCommonState.class);
+    if (state != null) {
+      state.setCommand(BlazeCommandName.RUN);
+    }
     blazeConfig.setGeneratedName();
   }
 }
diff --git a/java/src/com/google/idea/blaze/java/run/BlazeJavaRunConfigurationHandler.java b/java/src/com/google/idea/blaze/java/run/BlazeJavaRunConfigurationHandler.java
new file mode 100644
index 0000000..fd26ffd
--- /dev/null
+++ b/java/src/com/google/idea/blaze/java/run/BlazeJavaRunConfigurationHandler.java
@@ -0,0 +1,100 @@
+/*
+ * Copyright 2016 The Bazel Authors. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.idea.blaze.java.run;
+
+import com.google.idea.blaze.base.command.BlazeCommandName;
+import com.google.idea.blaze.base.run.BlazeCommandRunConfiguration;
+import com.google.idea.blaze.base.run.BlazeConfigurationNameBuilder;
+import com.google.idea.blaze.base.run.confighandler.BlazeCommandRunConfigurationHandler;
+import com.google.idea.blaze.base.run.confighandler.BlazeCommandRunConfigurationRunner;
+import com.google.idea.blaze.base.run.state.BlazeCommandRunConfigurationCommonState;
+import com.google.idea.blaze.base.settings.Blaze;
+import com.intellij.execution.Executor;
+import com.intellij.execution.configurations.RunConfiguration;
+import com.intellij.execution.configurations.RunProfileState;
+import com.intellij.execution.configurations.RuntimeConfigurationException;
+import com.intellij.execution.executors.DefaultDebugExecutor;
+import com.intellij.execution.runners.ExecutionEnvironment;
+import javax.annotation.Nullable;
+import javax.swing.Icon;
+
+/** Java-specific handler for {@link BlazeCommandRunConfiguration}s. */
+public final class BlazeJavaRunConfigurationHandler implements BlazeCommandRunConfigurationHandler {
+
+  private final String buildSystemName;
+  private final BlazeCommandRunConfigurationCommonState state;
+
+  public BlazeJavaRunConfigurationHandler(BlazeCommandRunConfiguration configuration) {
+    this.buildSystemName = Blaze.buildSystemName(configuration.getProject());
+    this.state = new BlazeCommandRunConfigurationCommonState(buildSystemName);
+  }
+
+  @Override
+  public BlazeCommandRunConfigurationCommonState getState() {
+    return state;
+  }
+
+  @Override
+  public BlazeCommandRunConfigurationRunner createRunner(
+      Executor executor, ExecutionEnvironment environment) {
+    return new BlazeJavaRunConfigurationRunner();
+  }
+
+  @Override
+  public void checkConfiguration() throws RuntimeConfigurationException {
+    state.validate(buildSystemName);
+  }
+
+  @Override
+  @Nullable
+  public String suggestedName(BlazeCommandRunConfiguration configuration) {
+    if (configuration.getTarget() == null) {
+      return null;
+    }
+    return new BlazeConfigurationNameBuilder(configuration).build();
+  }
+
+  @Override
+  @Nullable
+  public String getCommandName() {
+    BlazeCommandName command = state.getCommand();
+    return command != null ? command.toString() : null;
+  }
+
+  @Override
+  public String getHandlerName() {
+    return "Java Handler";
+  }
+
+  @Override
+  @Nullable
+  public Icon getExecutorIcon(RunConfiguration configuration, Executor executor) {
+    return null;
+  }
+
+  private static class BlazeJavaRunConfigurationRunner
+      implements BlazeCommandRunConfigurationRunner {
+    @Override
+    public RunProfileState getRunProfileState(Executor executor, ExecutionEnvironment environment) {
+      return new BlazeJavaRunProfileState(environment, executor instanceof DefaultDebugExecutor);
+    }
+
+    @Override
+    public boolean executeBeforeRunTask(ExecutionEnvironment environment) {
+      return true;
+    }
+  }
+}
diff --git a/java/src/com/google/idea/blaze/java/run/BlazeJavaRunConfigurationHandlerProvider.java b/java/src/com/google/idea/blaze/java/run/BlazeJavaRunConfigurationHandlerProvider.java
index 966fe91..ca6a022 100644
--- a/java/src/com/google/idea/blaze/java/run/BlazeJavaRunConfigurationHandlerProvider.java
+++ b/java/src/com/google/idea/blaze/java/run/BlazeJavaRunConfigurationHandlerProvider.java
@@ -18,15 +18,10 @@
 import com.google.common.collect.ImmutableSet;
 import com.google.idea.blaze.base.model.primitives.Kind;
 import com.google.idea.blaze.base.run.BlazeCommandRunConfiguration;
-import com.google.idea.blaze.base.run.confighandler.BlazeCommandGenericRunConfigurationHandler;
 import com.google.idea.blaze.base.run.confighandler.BlazeCommandRunConfigurationHandler;
 import com.google.idea.blaze.base.run.confighandler.BlazeCommandRunConfigurationHandlerProvider;
-import com.intellij.execution.Executor;
-import com.intellij.execution.configurations.RunProfileState;
-import com.intellij.execution.executors.DefaultDebugExecutor;
-import com.intellij.execution.runners.ExecutionEnvironment;
 
-/** Java-specific handler for {@link BlazeCommandRunConfiguration}s. */
+/** Java-specific handler provider for {@link BlazeCommandRunConfiguration}s. */
 public class BlazeJavaRunConfigurationHandlerProvider
     implements BlazeCommandRunConfigurationHandlerProvider {
 
@@ -52,31 +47,4 @@
     return "BlazeJavaRunConfigurationHandlerProvider";
   }
 
-  private static class BlazeJavaRunConfigurationHandler
-      extends BlazeCommandGenericRunConfigurationHandler {
-
-    BlazeJavaRunConfigurationHandler(BlazeCommandRunConfiguration configuration) {
-      super(configuration);
-    }
-
-    private BlazeJavaRunConfigurationHandler(
-        BlazeJavaRunConfigurationHandler other, BlazeCommandRunConfiguration configuration) {
-      super(other, configuration);
-    }
-
-    @Override
-    public BlazeJavaRunConfigurationHandler cloneFor(BlazeCommandRunConfiguration configuration) {
-      return new BlazeJavaRunConfigurationHandler(this, configuration);
-    }
-
-    @Override
-    public RunProfileState getState(Executor executor, ExecutionEnvironment environment) {
-      return new BlazeJavaRunProfileState(environment, executor instanceof DefaultDebugExecutor);
-    }
-
-    @Override
-    public String getHandlerName() {
-      return "Java Handler";
-    }
-  }
 }
diff --git a/java/src/com/google/idea/blaze/java/run/BlazeJavaRunProfileState.java b/java/src/com/google/idea/blaze/java/run/BlazeJavaRunProfileState.java
index 935a1e0..a712e65 100644
--- a/java/src/com/google/idea/blaze/java/run/BlazeJavaRunProfileState.java
+++ b/java/src/com/google/idea/blaze/java/run/BlazeJavaRunProfileState.java
@@ -21,7 +21,6 @@
 import com.google.idea.blaze.base.command.BlazeCommand;
 import com.google.idea.blaze.base.command.BlazeCommandName;
 import com.google.idea.blaze.base.command.BlazeFlags;
-import com.google.idea.blaze.base.ideinfo.RuleIdeInfo;
 import com.google.idea.blaze.base.issueparser.IssueOutputLineProcessor;
 import com.google.idea.blaze.base.metrics.Action;
 import com.google.idea.blaze.base.model.primitives.Kind;
@@ -29,9 +28,9 @@
 import com.google.idea.blaze.base.projectview.ProjectViewManager;
 import com.google.idea.blaze.base.projectview.ProjectViewSet;
 import com.google.idea.blaze.base.run.BlazeCommandRunConfiguration;
-import com.google.idea.blaze.base.run.confighandler.BlazeCommandGenericRunConfigurationHandler;
 import com.google.idea.blaze.base.run.processhandler.LineProcessingProcessAdapter;
 import com.google.idea.blaze.base.run.processhandler.ScopedBlazeProcessHandler;
+import com.google.idea.blaze.base.run.state.BlazeCommandRunConfigurationCommonState;
 import com.google.idea.blaze.base.scope.BlazeContext;
 import com.google.idea.blaze.base.scope.scopes.IdeaLogScope;
 import com.google.idea.blaze.base.scope.scopes.IssuesScope;
@@ -125,26 +124,22 @@
       ProjectViewSet projectViewSet,
       boolean debug) {
 
-    BlazeCommandGenericRunConfigurationHandler handler =
-        configuration.getHandlerIfType(BlazeCommandGenericRunConfigurationHandler.class);
-    assert handler != null;
+    BlazeCommandRunConfigurationCommonState handlerState =
+        configuration.getHandlerStateIfType(BlazeCommandRunConfigurationCommonState.class);
+    assert handlerState != null;
 
-    BlazeCommandName blazeCommand = handler.getCommand();
+    BlazeCommandName blazeCommand = handlerState.getCommand();
     assert blazeCommand != null;
     BlazeCommand.Builder command =
         BlazeCommand.builder(Blaze.getBuildSystem(project), blazeCommand)
-            .setBlazeBinary(handler.getBlazeBinary())
+            .setBlazeBinary(handlerState.getBlazeBinary())
             .addTargets(configuration.getTarget())
             .addBlazeFlags(BlazeFlags.buildFlags(project, projectViewSet))
-            .addBlazeFlags(handler.getAllBlazeFlags());
+            .addBlazeFlags(handlerState.getBlazeFlags());
 
     if (debug) {
-      boolean isJavaBinary = false;
-      RuleIdeInfo rule = configuration.getRuleForTarget();
-      if (rule != null && (rule.kind == Kind.JAVA_BINARY)) {
-        isJavaBinary = true;
-      }
-
+      Kind kind = configuration.getKindForTarget();
+      boolean isJavaBinary = kind == Kind.JAVA_BINARY;
       if (isJavaBinary) {
         command.addExeFlags(BlazeFlags.JAVA_BINARY_DEBUG);
       } else {
@@ -152,7 +147,7 @@
       }
     }
 
-    command.addExeFlags(handler.getAllExeFlags());
+    command.addExeFlags(handlerState.getExeFlags());
     return command.build();
   }
 }
diff --git a/java/src/com/google/idea/blaze/java/run/BlazeJavaTestRuleConfigurationFactory.java b/java/src/com/google/idea/blaze/java/run/BlazeJavaTestRunConfigurationFactory.java
similarity index 61%
rename from java/src/com/google/idea/blaze/java/run/BlazeJavaTestRuleConfigurationFactory.java
rename to java/src/com/google/idea/blaze/java/run/BlazeJavaTestRunConfigurationFactory.java
index 42c6256..0722e86 100644
--- a/java/src/com/google/idea/blaze/java/run/BlazeJavaTestRuleConfigurationFactory.java
+++ b/java/src/com/google/idea/blaze/java/run/BlazeJavaTestRunConfigurationFactory.java
@@ -17,21 +17,24 @@
 
 import com.google.idea.blaze.base.command.BlazeCommandName;
 import com.google.idea.blaze.base.ideinfo.RuleIdeInfo;
+import com.google.idea.blaze.base.ideinfo.RuleKey;
+import com.google.idea.blaze.base.model.BlazeProjectData;
 import com.google.idea.blaze.base.model.primitives.Kind;
+import com.google.idea.blaze.base.model.primitives.Label;
 import com.google.idea.blaze.base.run.BlazeCommandRunConfiguration;
 import com.google.idea.blaze.base.run.BlazeCommandRunConfigurationType;
-import com.google.idea.blaze.base.run.BlazeRuleConfigurationFactory;
-import com.google.idea.blaze.base.run.confighandler.BlazeCommandGenericRunConfigurationHandler;
-import com.google.idea.blaze.base.sync.projectview.WorkspaceLanguageSettings;
+import com.google.idea.blaze.base.run.BlazeRunConfigurationFactory;
+import com.google.idea.blaze.base.run.state.BlazeCommandRunConfigurationCommonState;
 import com.intellij.execution.configurations.ConfigurationFactory;
 import com.intellij.execution.configurations.RunConfiguration;
+import com.intellij.openapi.project.Project;
 
 /** Creates run configurations for java_test and android_robolectric_test. */
-public class BlazeJavaTestRuleConfigurationFactory extends BlazeRuleConfigurationFactory {
+public class BlazeJavaTestRunConfigurationFactory extends BlazeRunConfigurationFactory {
   @Override
-  public boolean handlesRule(
-      WorkspaceLanguageSettings workspaceLanguageSettings, RuleIdeInfo rule) {
-    return rule.kindIsOneOf(Kind.JAVA_TEST, Kind.ANDROID_ROBOLECTRIC_TEST);
+  public boolean handlesTarget(Project project, BlazeProjectData blazeProjectData, Label target) {
+    RuleIdeInfo rule = blazeProjectData.ruleMap.get(RuleKey.forPlainTarget(target));
+    return rule != null && rule.kindIsOneOf(Kind.JAVA_TEST, Kind.ANDROID_ROBOLECTRIC_TEST);
   }
 
   @Override
@@ -40,14 +43,15 @@
   }
 
   @Override
-  public void setupConfiguration(RunConfiguration configuration, RuleIdeInfo rule) {
+  public void setupConfiguration(RunConfiguration configuration, Label target) {
     final BlazeCommandRunConfiguration blazeConfig = (BlazeCommandRunConfiguration) configuration;
-    blazeConfig.setTarget(rule.label);
+    blazeConfig.setTarget(target);
 
-    BlazeCommandGenericRunConfigurationHandler handler =
-        (BlazeCommandGenericRunConfigurationHandler) blazeConfig.getHandler();
-    handler.setCommand(BlazeCommandName.TEST);
-
+    BlazeCommandRunConfigurationCommonState state =
+        blazeConfig.getHandlerStateIfType(BlazeCommandRunConfigurationCommonState.class);
+    if (state != null) {
+      state.setCommand(BlazeCommandName.TEST);
+    }
     blazeConfig.setGeneratedName();
   }
 }
diff --git a/java/src/com/google/idea/blaze/java/run/producers/BlazeJavaMainClassRunConfigurationProducer.java b/java/src/com/google/idea/blaze/java/run/producers/BlazeJavaMainClassRunConfigurationProducer.java
index 72c35f6..43ba6d0 100644
--- a/java/src/com/google/idea/blaze/java/run/producers/BlazeJavaMainClassRunConfigurationProducer.java
+++ b/java/src/com/google/idea/blaze/java/run/producers/BlazeJavaMainClassRunConfigurationProducer.java
@@ -18,15 +18,15 @@
 import com.google.common.collect.ImmutableCollection;
 import com.google.idea.blaze.base.command.BlazeCommandName;
 import com.google.idea.blaze.base.ideinfo.RuleIdeInfo;
+import com.google.idea.blaze.base.ideinfo.RuleKey;
 import com.google.idea.blaze.base.model.BlazeProjectData;
-import com.google.idea.blaze.base.model.RuleMap;
 import com.google.idea.blaze.base.model.primitives.Kind;
 import com.google.idea.blaze.base.model.primitives.Label;
 import com.google.idea.blaze.base.rulemaps.SourceToRuleMap;
 import com.google.idea.blaze.base.run.BlazeCommandRunConfiguration;
 import com.google.idea.blaze.base.run.BlazeCommandRunConfigurationType;
-import com.google.idea.blaze.base.run.confighandler.BlazeCommandGenericRunConfigurationHandler;
 import com.google.idea.blaze.base.run.producers.BlazeRunConfigurationProducer;
+import com.google.idea.blaze.base.run.state.BlazeCommandRunConfigurationCommonState;
 import com.google.idea.blaze.base.sync.data.BlazeProjectDataManager;
 import com.google.idea.blaze.java.run.RunUtil;
 import com.intellij.execution.JavaExecutionUtil;
@@ -40,7 +40,9 @@
 import com.intellij.psi.PsiMethod;
 import com.intellij.psi.util.PsiMethodUtil;
 import java.io.File;
+import java.util.List;
 import java.util.Objects;
+import java.util.stream.Collectors;
 import org.jetbrains.annotations.Nullable;
 
 /** Creates run configurations for Java main classes sourced by java_binary targets. */
@@ -74,12 +76,12 @@
       return false;
     }
     configuration.setTarget(label);
-    BlazeCommandGenericRunConfigurationHandler handler =
-        configuration.getHandlerIfType(BlazeCommandGenericRunConfigurationHandler.class);
-    if (handler == null) {
+    BlazeCommandRunConfigurationCommonState handlerState =
+        configuration.getHandlerStateIfType(BlazeCommandRunConfigurationCommonState.class);
+    if (handlerState == null) {
       return false;
     }
-    handler.setCommand(BlazeCommandName.RUN);
+    handlerState.setCommand(BlazeCommandName.RUN);
     configuration.setGeneratedName();
     return true;
   }
@@ -87,12 +89,12 @@
   @Override
   protected boolean doIsConfigFromContext(
       BlazeCommandRunConfiguration configuration, ConfigurationContext context) {
-    BlazeCommandGenericRunConfigurationHandler handler =
-        configuration.getHandlerIfType(BlazeCommandGenericRunConfigurationHandler.class);
-    if (handler == null) {
+    BlazeCommandRunConfigurationCommonState handlerState =
+        configuration.getHandlerStateIfType(BlazeCommandRunConfigurationCommonState.class);
+    if (handlerState == null) {
       return false;
     }
-    if (!Objects.equals(handler.getCommand(), BlazeCommandName.RUN)) {
+    if (!Objects.equals(handlerState.getCommand(), BlazeCommandName.RUN)) {
       return false;
     }
     PsiClass mainClass = getMainClass(context);
@@ -126,20 +128,25 @@
   @Nullable
   private static Label getRuleLabel(Project project, PsiClass mainClass) {
     File mainClassFile = RunUtil.getFileForClass(mainClass);
-    ImmutableCollection<Label> labels =
-        SourceToRuleMap.getInstance(project).getTargetsForSourceFile(mainClassFile);
+    ImmutableCollection<RuleKey> ruleKeys =
+        SourceToRuleMap.getInstance(project).getRulesForSourceFile(mainClassFile);
     BlazeProjectData blazeProjectData =
         BlazeProjectDataManager.getInstance(project).getBlazeProjectData();
     if (blazeProjectData == null) {
       return null;
     }
-    RuleMap ruleMap = blazeProjectData.ruleMap;
-    for (Label label : labels) {
-      RuleIdeInfo rule = ruleMap.get(label);
+    List<RuleIdeInfo> rules =
+        ruleKeys
+            .stream()
+            .map(blazeProjectData.ruleMap::get)
+            .filter(Objects::nonNull)
+            .filter(RuleIdeInfo::isPlainTarget)
+            .collect(Collectors.toList());
+    for (RuleIdeInfo rule : rules) {
       if (rule.kind == Kind.JAVA_BINARY) {
         // Best-effort guess: the main_class attribute isn't exposed, but assume
         // mainClass is the main_class because it is sourced by the java_binary.
-        return label;
+        return rule.label;
       }
     }
     return null;
diff --git a/java/src/com/google/idea/blaze/java/run/producers/BlazeJavaTestClassConfigurationProducer.java b/java/src/com/google/idea/blaze/java/run/producers/BlazeJavaTestClassConfigurationProducer.java
index c9c4c3e..cd1764c 100644
--- a/java/src/com/google/idea/blaze/java/run/producers/BlazeJavaTestClassConfigurationProducer.java
+++ b/java/src/com/google/idea/blaze/java/run/producers/BlazeJavaTestClassConfigurationProducer.java
@@ -23,8 +23,8 @@
 import com.google.idea.blaze.base.run.BlazeCommandRunConfiguration;
 import com.google.idea.blaze.base.run.BlazeCommandRunConfigurationType;
 import com.google.idea.blaze.base.run.BlazeConfigurationNameBuilder;
-import com.google.idea.blaze.base.run.confighandler.BlazeCommandGenericRunConfigurationHandler;
 import com.google.idea.blaze.base.run.producers.BlazeRunConfigurationProducer;
+import com.google.idea.blaze.base.run.state.BlazeCommandRunConfigurationCommonState;
 import com.google.idea.blaze.java.run.RunUtil;
 import com.intellij.execution.JavaExecutionUtil;
 import com.intellij.execution.Location;
@@ -76,12 +76,12 @@
     }
 
     configuration.setTarget(rule.label);
-    BlazeCommandGenericRunConfigurationHandler handler =
-        configuration.getHandlerIfType(BlazeCommandGenericRunConfigurationHandler.class);
-    if (handler == null) {
+    BlazeCommandRunConfigurationCommonState handlerState =
+        configuration.getHandlerStateIfType(BlazeCommandRunConfigurationCommonState.class);
+    if (handlerState == null) {
       return false;
     }
-    handler.setCommand(BlazeCommandName.TEST);
+    handlerState.setCommand(BlazeCommandName.TEST);
 
     ImmutableList.Builder<String> flags = ImmutableList.builder();
 
@@ -91,9 +91,9 @@
     }
 
     flags.add(BlazeFlags.TEST_OUTPUT_STREAMED);
-    flags.addAll(handler.getAllBlazeFlags());
+    flags.addAll(handlerState.getBlazeFlags());
 
-    handler.setBlazeFlags(flags.build());
+    handlerState.setBlazeFlags(flags.build());
 
     BlazeConfigurationNameBuilder nameBuilder = new BlazeConfigurationNameBuilder(configuration);
     nameBuilder.setTargetString(testClass.getName());
@@ -132,15 +132,15 @@
 
   private boolean checkIfAttributesAreTheSame(
       @NotNull BlazeCommandRunConfiguration configuration, @NotNull PsiClass testClass) {
-    BlazeCommandGenericRunConfigurationHandler handler =
-        configuration.getHandlerIfType(BlazeCommandGenericRunConfigurationHandler.class);
-    if (handler == null) {
+    BlazeCommandRunConfigurationCommonState handlerState =
+        configuration.getHandlerStateIfType(BlazeCommandRunConfigurationCommonState.class);
+    if (handlerState == null) {
       return false;
     }
-    if (!Objects.equals(handler.getCommand(), BlazeCommandName.TEST)) {
+    if (!Objects.equals(handlerState.getCommand(), BlazeCommandName.TEST)) {
       return false;
     }
-    List<String> flags = handler.getAllBlazeFlags();
+    List<String> flags = handlerState.getBlazeFlags();
 
     return flags.contains(BlazeFlags.testFilterFlagForClass(testClass.getQualifiedName()));
   }
diff --git a/java/src/com/google/idea/blaze/java/run/producers/BlazeJavaTestMethodConfigurationProducer.java b/java/src/com/google/idea/blaze/java/run/producers/BlazeJavaTestMethodConfigurationProducer.java
index 4b67696..d10be31 100644
--- a/java/src/com/google/idea/blaze/java/run/producers/BlazeJavaTestMethodConfigurationProducer.java
+++ b/java/src/com/google/idea/blaze/java/run/producers/BlazeJavaTestMethodConfigurationProducer.java
@@ -23,8 +23,8 @@
 import com.google.idea.blaze.base.run.BlazeCommandRunConfiguration;
 import com.google.idea.blaze.base.run.BlazeCommandRunConfigurationType;
 import com.google.idea.blaze.base.run.BlazeConfigurationNameBuilder;
-import com.google.idea.blaze.base.run.confighandler.BlazeCommandGenericRunConfigurationHandler;
 import com.google.idea.blaze.base.run.producers.BlazeRunConfigurationProducer;
+import com.google.idea.blaze.base.run.state.BlazeCommandRunConfigurationCommonState;
 import com.google.idea.blaze.java.run.RunUtil;
 import com.intellij.execution.actions.ConfigurationContext;
 import com.intellij.execution.junit.JUnitUtil;
@@ -88,19 +88,19 @@
     }
 
     configuration.setTarget(rule.label);
-    BlazeCommandGenericRunConfigurationHandler handler =
-        configuration.getHandlerIfType(BlazeCommandGenericRunConfigurationHandler.class);
-    if (handler == null) {
+    BlazeCommandRunConfigurationCommonState handlerState =
+        configuration.getHandlerStateIfType(BlazeCommandRunConfigurationCommonState.class);
+    if (handlerState == null) {
       return false;
     }
-    handler.setCommand(BlazeCommandName.TEST);
+    handlerState.setCommand(BlazeCommandName.TEST);
 
     ImmutableList.Builder<String> flags = ImmutableList.builder();
     flags.add(methodInfo.testFilterFlag);
     flags.add(BlazeFlags.TEST_OUTPUT_STREAMED);
-    flags.addAll(handler.getAllBlazeFlags());
+    flags.addAll(handlerState.getBlazeFlags());
 
-    handler.setBlazeFlags(flags.build());
+    handlerState.setBlazeFlags(flags.build());
 
     BlazeConfigurationNameBuilder nameBuilder = new BlazeConfigurationNameBuilder(configuration);
     nameBuilder.setTargetString(
@@ -115,12 +115,12 @@
   @Override
   protected boolean doIsConfigFromContext(
       @NotNull BlazeCommandRunConfiguration configuration, @NotNull ConfigurationContext context) {
-    BlazeCommandGenericRunConfigurationHandler handler =
-        configuration.getHandlerIfType(BlazeCommandGenericRunConfigurationHandler.class);
-    if (handler == null) {
+    BlazeCommandRunConfigurationCommonState handlerState =
+        configuration.getHandlerStateIfType(BlazeCommandRunConfigurationCommonState.class);
+    if (handlerState == null) {
       return false;
     }
-    if (!Objects.equals(handler.getCommand(), BlazeCommandName.TEST)) {
+    if (!Objects.equals(handlerState.getCommand(), BlazeCommandName.TEST)) {
       return false;
     }
 
@@ -129,7 +129,7 @@
       return false;
     }
 
-    List<String> flags = handler.getAllBlazeFlags();
+    List<String> flags = handlerState.getBlazeFlags();
     return flags.contains(methodInfo.testFilterFlag);
   }
 
diff --git a/java/src/com/google/idea/blaze/java/sync/BlazeJavaSyncPlugin.java b/java/src/com/google/idea/blaze/java/sync/BlazeJavaSyncPlugin.java
index 9d6eb17..257dcab 100644
--- a/java/src/com/google/idea/blaze/java/sync/BlazeJavaSyncPlugin.java
+++ b/java/src/com/google/idea/blaze/java/sync/BlazeJavaSyncPlugin.java
@@ -19,8 +19,8 @@
 import com.google.common.collect.ImmutableSet;
 import com.google.idea.blaze.base.ideinfo.ArtifactLocation;
 import com.google.idea.blaze.base.ideinfo.LibraryArtifact;
+import com.google.idea.blaze.base.ideinfo.RuleMap;
 import com.google.idea.blaze.base.model.BlazeProjectData;
-import com.google.idea.blaze.base.model.RuleMap;
 import com.google.idea.blaze.base.model.SyncState;
 import com.google.idea.blaze.base.model.primitives.LanguageClass;
 import com.google.idea.blaze.base.model.primitives.WorkspaceRoot;
@@ -43,6 +43,7 @@
 import com.google.idea.blaze.java.projectview.ExcludedLibrarySection;
 import com.google.idea.blaze.java.projectview.JavaLanguageLevelSection;
 import com.google.idea.blaze.java.sync.importer.BlazeJavaWorkspaceImporter;
+import com.google.idea.blaze.java.sync.importer.JavaSourceFilter;
 import com.google.idea.blaze.java.sync.jdeps.JdepsFileReader;
 import com.google.idea.blaze.java.sync.jdeps.JdepsMap;
 import com.google.idea.blaze.java.sync.model.BlazeJarLibrary;
@@ -54,7 +55,6 @@
 import com.google.idea.blaze.java.sync.projectstructure.SourceFolderEditor;
 import com.google.idea.blaze.java.sync.workingset.JavaWorkingSet;
 import com.intellij.openapi.application.ApplicationManager;
-import com.intellij.openapi.diagnostic.Logger;
 import com.intellij.openapi.module.Module;
 import com.intellij.openapi.module.ModuleType;
 import com.intellij.openapi.module.StdModuleTypes;
@@ -73,7 +73,6 @@
 
 /** Sync support for Java. */
 public class BlazeJavaSyncPlugin extends BlazeSyncPlugin.Adapter {
-  private static final Logger LOG = Logger.getInstance(BlazeJavaSyncPlugin.class);
   private final JdepsFileReader jdepsFileReader = new JdepsFileReader();
 
   @Nullable
@@ -109,6 +108,7 @@
       BlazeRoots blazeRoots,
       @Nullable WorkingSet workingSet,
       WorkspacePathResolver workspacePathResolver,
+      ArtifactLocationDecoder artifactLocationDecoder,
       RuleMap ruleMap,
       SyncState.Builder syncStateBuilder,
       @Nullable SyncState previousSyncState) {
@@ -117,9 +117,17 @@
       javaWorkingSet = new JavaWorkingSet(workspaceRoot, workingSet);
     }
 
+    JavaSourceFilter sourceFilter =
+        new JavaSourceFilter(project, workspaceRoot, projectViewSet, ruleMap);
+
     JdepsMap jdepsMap =
         jdepsFileReader.loadJdepsFiles(
-            project, context, ruleMap, syncStateBuilder, previousSyncState);
+            project,
+            context,
+            artifactLocationDecoder,
+            sourceFilter.getSourceRules(),
+            syncStateBuilder,
+            previousSyncState);
     if (context.isCancelled()) {
       return;
     }
@@ -127,14 +135,14 @@
     BlazeJavaWorkspaceImporter blazeJavaWorkspaceImporter =
         new BlazeJavaWorkspaceImporter(
             project,
-            context,
             workspaceRoot,
             projectViewSet,
             workspaceLanguageSettings,
             ruleMap,
+            sourceFilter,
             jdepsMap,
             javaWorkingSet,
-            new ArtifactLocationDecoder(blazeRoots, workspacePathResolver));
+            artifactLocationDecoder);
     BlazeJavaImportResult importResult =
         Scope.push(
             context,
diff --git a/java/src/com/google/idea/blaze/java/sync/DuplicateSourceDetector.java b/java/src/com/google/idea/blaze/java/sync/DuplicateSourceDetector.java
index 9357346..13d5fbf 100644
--- a/java/src/com/google/idea/blaze/java/sync/DuplicateSourceDetector.java
+++ b/java/src/com/google/idea/blaze/java/sync/DuplicateSourceDetector.java
@@ -20,7 +20,7 @@
 import com.google.common.collect.Multimap;
 import com.google.common.collect.Sets;
 import com.google.idea.blaze.base.ideinfo.ArtifactLocation;
-import com.google.idea.blaze.base.model.primitives.Label;
+import com.google.idea.blaze.base.ideinfo.RuleKey;
 import com.google.idea.blaze.base.scope.BlazeContext;
 import com.google.idea.blaze.base.scope.output.PerformanceWarning;
 import java.util.Collection;
@@ -30,30 +30,30 @@
 
 /** Detects and reports duplicate sources */
 public class DuplicateSourceDetector {
-  Multimap<ArtifactLocation, Label> artifacts = ArrayListMultimap.create();
+  Multimap<ArtifactLocation, RuleKey> artifacts = ArrayListMultimap.create();
 
-  public void add(Label label, ArtifactLocation artifactLocation) {
-    artifacts.put(artifactLocation, label);
+  public void add(RuleKey ruleKey, ArtifactLocation artifactLocation) {
+    artifacts.put(artifactLocation, ruleKey);
   }
 
   static class Duplicate {
     final ArtifactLocation artifactLocation;
-    final Collection<Label> labels;
+    final Collection<RuleKey> rules;
 
-    public Duplicate(ArtifactLocation artifactLocation, Collection<Label> labels) {
+    public Duplicate(ArtifactLocation artifactLocation, Collection<RuleKey> rules) {
       this.artifactLocation = artifactLocation;
-      this.labels = labels;
+      this.rules = rules;
     }
   }
 
   public void reportDuplicates(BlazeContext context) {
     List<Duplicate> duplicates = Lists.newArrayList();
     for (ArtifactLocation key : artifacts.keySet()) {
-      Collection<Label> labels = artifacts.get(key);
+      Collection<RuleKey> labels = artifacts.get(key);
       if (labels.size() > 1) {
 
         // Workaround for aspect bug. Can be removed after the next blaze release, as of May 27 2016
-        Set<Label> labelSet = Sets.newHashSet(labels);
+        Set<RuleKey> labelSet = Sets.newHashSet(labels);
         if (labelSet.size() > 1) {
           duplicates.add(new Duplicate(key, labelSet));
         }
@@ -76,8 +76,8 @@
       ArtifactLocation artifactLocation = duplicate.artifactLocation;
       context.output(new PerformanceWarning("  Source: " + artifactLocation.getRelativePath()));
       context.output(new PerformanceWarning("  Consumed by rules:"));
-      for (Label label : duplicate.labels) {
-        context.output(new PerformanceWarning("    " + label));
+      for (RuleKey ruleKey : duplicate.rules) {
+        context.output(new PerformanceWarning("    " + ruleKey.label));
       }
       context.output(new PerformanceWarning("")); // Newline
     }
diff --git a/java/src/com/google/idea/blaze/java/sync/JavaPrefetchFileSource.java b/java/src/com/google/idea/blaze/java/sync/JavaPrefetchFileSource.java
index b77741a..21b3cd2 100644
--- a/java/src/com/google/idea/blaze/java/sync/JavaPrefetchFileSource.java
+++ b/java/src/com/google/idea/blaze/java/sync/JavaPrefetchFileSource.java
@@ -18,6 +18,7 @@
 import com.google.common.collect.ImmutableSet;
 import com.google.idea.blaze.base.model.BlazeProjectData;
 import com.google.idea.blaze.base.prefetch.PrefetchFileSource;
+import com.google.idea.blaze.base.sync.workspace.ArtifactLocationDecoder;
 import com.google.idea.blaze.java.libraries.JarCache;
 import com.google.idea.blaze.java.libraries.SourceJarManager;
 import com.google.idea.blaze.java.settings.BlazeJavaUserSettings;
@@ -46,17 +47,18 @@
         BlazeJavaUserSettings.getInstance().getAttachSourcesByDefault();
     SourceJarManager sourceJarManager = SourceJarManager.getInstance(project);
     Collection<BlazeLibrary> libraries = BlazeLibraryCollector.getLibraries(blazeProjectData);
+    ArtifactLocationDecoder artifactLocationDecoder = blazeProjectData.artifactLocationDecoder;
     for (BlazeLibrary library : libraries) {
       if (!(library instanceof BlazeJarLibrary)) {
         continue;
       }
       BlazeJarLibrary jarLibrary = (BlazeJarLibrary) library;
-      files.add(jarLibrary.libraryArtifact.jarForIntellijLibrary().getFile());
+      files.add(artifactLocationDecoder.decode(jarLibrary.libraryArtifact.jarForIntellijLibrary()));
 
       boolean attachSourceJar =
           attachSourcesByDefault || sourceJarManager.hasSourceJarAttached(jarLibrary.key);
       if (attachSourceJar && jarLibrary.libraryArtifact.sourceJar != null) {
-        files.add(jarLibrary.libraryArtifact.sourceJar.getFile());
+        files.add(artifactLocationDecoder.decode(jarLibrary.libraryArtifact.sourceJar));
       }
     }
   }
diff --git a/java/src/com/google/idea/blaze/java/sync/importer/BlazeJavaWorkspaceImporter.java b/java/src/com/google/idea/blaze/java/sync/importer/BlazeJavaWorkspaceImporter.java
index 61d7155..4ef5bf8 100644
--- a/java/src/com/google/idea/blaze/java/sync/importer/BlazeJavaWorkspaceImporter.java
+++ b/java/src/com/google/idea/blaze/java/sync/importer/BlazeJavaWorkspaceImporter.java
@@ -29,8 +29,8 @@
 import com.google.idea.blaze.base.ideinfo.LibraryArtifact;
 import com.google.idea.blaze.base.ideinfo.ProtoLibraryLegacyInfo;
 import com.google.idea.blaze.base.ideinfo.RuleIdeInfo;
-import com.google.idea.blaze.base.model.RuleMap;
-import com.google.idea.blaze.base.model.primitives.Kind;
+import com.google.idea.blaze.base.ideinfo.RuleKey;
+import com.google.idea.blaze.base.ideinfo.RuleMap;
 import com.google.idea.blaze.base.model.primitives.Label;
 import com.google.idea.blaze.base.model.primitives.WorkspaceRoot;
 import com.google.idea.blaze.base.projectview.ProjectViewSet;
@@ -38,7 +38,6 @@
 import com.google.idea.blaze.base.scope.output.PrintOutput;
 import com.google.idea.blaze.base.settings.Blaze;
 import com.google.idea.blaze.base.sync.projectview.ImportRoots;
-import com.google.idea.blaze.base.sync.projectview.ProjectViewRuleImportFilter;
 import com.google.idea.blaze.base.sync.projectview.SourceTestConfig;
 import com.google.idea.blaze.base.sync.projectview.WorkspaceLanguageSettings;
 import com.google.idea.blaze.base.sync.workspace.ArtifactLocationDecoder;
@@ -52,10 +51,7 @@
 import com.google.idea.blaze.java.sync.source.SourceArtifact;
 import com.google.idea.blaze.java.sync.source.SourceDirectoryCalculator;
 import com.google.idea.blaze.java.sync.workingset.JavaWorkingSet;
-import com.google.idea.common.experiments.BoolExperiment;
-import com.intellij.openapi.diagnostic.Logger;
 import com.intellij.openapi.project.Project;
-import java.io.File;
 import java.util.Collection;
 import java.util.List;
 import java.util.Map;
@@ -65,13 +61,7 @@
 
 /** Builds a BlazeWorkspace. */
 public final class BlazeJavaWorkspaceImporter {
-  private static final Logger LOG = Logger.getInstance(BlazeJavaWorkspaceImporter.class);
-
-  private static final BoolExperiment NO_EMPTY_SOURCE_RULES =
-      new BoolExperiment("no.empty.source.rules", true);
-
   private final Project project;
-  private final BlazeContext context;
   private final WorkspaceRoot workspaceRoot;
   private final ImportRoots importRoots;
   private final RuleMap ruleMap;
@@ -79,87 +69,39 @@
   private final JdepsMap jdepsMap;
   @Nullable private final JavaWorkingSet workingSet;
   private final ArtifactLocationDecoder artifactLocationDecoder;
-  private final ProjectViewRuleImportFilter importFilter;
   private final DuplicateSourceDetector duplicateSourceDetector = new DuplicateSourceDetector();
   private final Collection<BlazeJavaSyncAugmenter> augmenters;
+  private final JavaSourceFilter sourceFilter;
 
   public BlazeJavaWorkspaceImporter(
       Project project,
-      BlazeContext context,
       WorkspaceRoot workspaceRoot,
       ProjectViewSet projectViewSet,
       WorkspaceLanguageSettings workspaceLanguageSettings,
       RuleMap ruleMap,
+      JavaSourceFilter sourceFilter,
       JdepsMap jdepsMap,
       @Nullable JavaWorkingSet workingSet,
       ArtifactLocationDecoder artifactLocationDecoder) {
     this.project = project;
-    this.context = context;
     this.workspaceRoot = workspaceRoot;
     this.importRoots =
         ImportRoots.builder(workspaceRoot, Blaze.getBuildSystem(project))
             .add(projectViewSet)
             .build();
     this.ruleMap = ruleMap;
+    this.sourceFilter = sourceFilter;
     this.jdepsMap = jdepsMap;
     this.workingSet = workingSet;
     this.artifactLocationDecoder = artifactLocationDecoder;
-    this.importFilter = new ProjectViewRuleImportFilter(project, workspaceRoot, projectViewSet);
     this.sourceTestConfig = new SourceTestConfig(projectViewSet);
     this.augmenters = BlazeJavaSyncAugmenter.getActiveSyncAgumenters(workspaceLanguageSettings);
   }
 
   public BlazeJavaImportResult importWorkspace(BlazeContext context) {
-    List<RuleIdeInfo> includedRules =
-        ruleMap
-            .rules()
-            .stream()
-            .filter(rule -> !importFilter.excludeTarget(rule))
-            .collect(Collectors.toList());
-
-    List<RuleIdeInfo> javaRules =
-        includedRules
-            .stream()
-            .filter(rule -> rule.javaRuleIdeInfo != null)
-            .collect(Collectors.toList());
-
-    Map<Label, Collection<ArtifactLocation>> ruleToJavaSources = Maps.newHashMap();
-    for (RuleIdeInfo rule : javaRules) {
-      List<ArtifactLocation> javaSources =
-          rule.sources
-              .stream()
-              .filter(source -> source.getRelativePath().endsWith(".java"))
-              .collect(Collectors.toList());
-      ruleToJavaSources.put(rule.label, javaSources);
-    }
-
-    boolean noEmptySourceRules = NO_EMPTY_SOURCE_RULES.getValue();
-    List<RuleIdeInfo> sourceRules = Lists.newArrayList();
-    List<RuleIdeInfo> libraryRules = Lists.newArrayList();
-    for (RuleIdeInfo rule : javaRules) {
-      boolean importAsSource =
-          importFilter.isSourceRule(rule)
-              && canImportAsSource(rule)
-              && (noEmptySourceRules
-                  ? anyNonGeneratedSources(ruleToJavaSources.get(rule.label))
-                  : !allSourcesGenerated(ruleToJavaSources.get(rule.label)));
-
-      if (importAsSource) {
-        sourceRules.add(rule);
-      } else {
-        libraryRules.add(rule);
-      }
-    }
-
-    List<RuleIdeInfo> protoLibraries =
-        includedRules
-            .stream()
-            .filter(rule -> rule.kind == Kind.PROTO_LIBRARY)
-            .collect(Collectors.toList());
-
     WorkspaceBuilder workspaceBuilder = new WorkspaceBuilder();
-    for (RuleIdeInfo rule : sourceRules) {
-      addRuleAsSource(workspaceBuilder, rule, ruleToJavaSources.get(rule.label));
+    for (RuleIdeInfo rule : sourceFilter.sourceRules) {
+      addRuleAsSource(workspaceBuilder, rule, sourceFilter.ruleToJavaSources.get(rule.key));
     }
 
     SourceDirectoryCalculator sourceDirectoryCalculator = new SourceDirectoryCalculator();
@@ -181,7 +123,8 @@
     context.output(PrintOutput.log("Java content entry count: " + totalContentEntryCount));
 
     ImmutableMap<LibraryKey, BlazeJarLibrary> libraries =
-        buildLibraries(workspaceBuilder, ruleMap, libraryRules, protoLibraries);
+        buildLibraries(
+            workspaceBuilder, ruleMap, sourceFilter.libraryRules, sourceFilter.protoLibraries);
 
     duplicateSourceDetector.reportDuplicates(context);
 
@@ -196,31 +139,19 @@
         sourceVersion);
   }
 
-  private boolean canImportAsSource(RuleIdeInfo rule) {
-    return !rule.kindIsOneOf(Kind.JAVA_WRAP_CC, Kind.JAVA_IMPORT);
-  }
-
-  private boolean allSourcesGenerated(Collection<ArtifactLocation> sources) {
-    return !sources.isEmpty() && sources.stream().allMatch(ArtifactLocation::isGenerated);
-  }
-
-  private boolean anyNonGeneratedSources(Collection<ArtifactLocation> sources) {
-    return sources.stream().anyMatch(ArtifactLocation::isSource);
-  }
-
   private ImmutableMap<LibraryKey, BlazeJarLibrary> buildLibraries(
       WorkspaceBuilder workspaceBuilder,
       RuleMap ruleMap,
       List<RuleIdeInfo> libraryRules,
       List<RuleIdeInfo> protoLibraries) {
     // Build library maps
-    Multimap<Label, BlazeJarLibrary> labelToLibrary = ArrayListMultimap.create();
+    Multimap<RuleKey, BlazeJarLibrary> ruleKeyToLibrary = ArrayListMultimap.create();
     Map<String, BlazeJarLibrary> jdepsPathToLibrary = Maps.newHashMap();
 
     // Add any output jars from source rules
-    for (Label label : workspaceBuilder.outputJarsFromSourceRules.keySet()) {
-      Collection<BlazeJarLibrary> jars = workspaceBuilder.outputJarsFromSourceRules.get(label);
-      labelToLibrary.putAll(label, jars);
+    for (RuleKey key : workspaceBuilder.outputJarsFromSourceRules.keySet()) {
+      Collection<BlazeJarLibrary> jars = workspaceBuilder.outputJarsFromSourceRules.get(key);
+      ruleKeyToLibrary.putAll(key, jars);
       for (BlazeJarLibrary library : jars) {
         addLibraryToJdeps(jdepsPathToLibrary, library);
       }
@@ -236,10 +167,10 @@
       Collection<BlazeJarLibrary> libraries =
           allJars
               .stream()
-              .map(library -> new BlazeJarLibrary(library, rule.label))
+              .map(library -> new BlazeJarLibrary(library, rule.key))
               .collect(Collectors.toList());
 
-      labelToLibrary.putAll(rule.label, libraries);
+      ruleKeyToLibrary.putAll(rule.key, libraries);
       for (BlazeJarLibrary library : libraries) {
         addLibraryToJdeps(jdepsPathToLibrary, library);
       }
@@ -256,7 +187,7 @@
               protoLibraryLegacyInfo.jarsV1,
               protoLibraryLegacyInfo.jarsMutable,
               protoLibraryLegacyInfo.jarsImmutable)) {
-        addLibraryToJdeps(jdepsPathToLibrary, new BlazeJarLibrary(libraryArtifact, rule.label));
+        addLibraryToJdeps(jdepsPathToLibrary, new BlazeJarLibrary(libraryArtifact, rule.key));
       }
     }
 
@@ -271,8 +202,8 @@
     }
 
     // Collect jars referenced by direct deps from your working set
-    for (Label deps : workspaceBuilder.directDeps) {
-      for (BlazeJarLibrary library : labelToLibrary.get(deps)) {
+    for (RuleKey deps : workspaceBuilder.directDeps) {
+      for (BlazeJarLibrary library : ruleKeyToLibrary.get(deps)) {
         result.put(library.key, library);
       }
     }
@@ -290,11 +221,11 @@
 
   private void addProtoLegacyLibrariesFromDirectDeps(
       WorkspaceBuilder workspaceBuilder, RuleMap ruleMap, Map<LibraryKey, BlazeJarLibrary> result) {
-    List<Label> version1Roots = Lists.newArrayList();
-    List<Label> immutableRoots = Lists.newArrayList();
-    List<Label> mutableRoots = Lists.newArrayList();
-    for (Label label : workspaceBuilder.directDeps) {
-      RuleIdeInfo rule = ruleMap.get(label);
+    List<RuleKey> version1Roots = Lists.newArrayList();
+    List<RuleKey> immutableRoots = Lists.newArrayList();
+    List<RuleKey> mutableRoots = Lists.newArrayList();
+    for (RuleKey ruleKey : workspaceBuilder.directDeps) {
+      RuleIdeInfo rule = ruleMap.get(ruleKey);
       if (rule == null) {
         continue;
       }
@@ -304,17 +235,17 @@
       }
       switch (protoLibraryLegacyInfo.apiFlavor) {
         case VERSION_1:
-          version1Roots.add(label);
+          version1Roots.add(ruleKey);
           break;
         case IMMUTABLE:
-          immutableRoots.add(label);
+          immutableRoots.add(ruleKey);
           break;
         case MUTABLE:
-          mutableRoots.add(label);
+          mutableRoots.add(ruleKey);
           break;
         case BOTH:
-          mutableRoots.add(label);
-          immutableRoots.add(label);
+          mutableRoots.add(ruleKey);
+          immutableRoots.add(ruleKey);
           break;
         default:
           // Can't happen
@@ -333,15 +264,15 @@
   private void addProtoLegacyLibrariesFromDirectDepsForFlavor(
       RuleMap ruleMap,
       ProtoLibraryLegacyInfo.ApiFlavor apiFlavor,
-      List<Label> roots,
+      List<RuleKey> roots,
       Map<LibraryKey, BlazeJarLibrary> result) {
-    Set<Label> seen = Sets.newHashSet();
+    Set<RuleKey> seen = Sets.newHashSet();
     while (!roots.isEmpty()) {
-      Label label = roots.remove(roots.size() - 1);
-      if (!seen.add(label)) {
+      RuleKey ruleKey = roots.remove(roots.size() - 1);
+      if (!seen.add(ruleKey)) {
         continue;
       }
-      RuleIdeInfo rule = ruleMap.get(label);
+      RuleIdeInfo rule = ruleMap.get(ruleKey);
       if (rule == null) {
         continue;
       }
@@ -368,12 +299,14 @@
 
       if (libraries != null) {
         for (LibraryArtifact libraryArtifact : libraries) {
-          BlazeJarLibrary library = new BlazeJarLibrary(libraryArtifact, label);
+          BlazeJarLibrary library = new BlazeJarLibrary(libraryArtifact, ruleKey);
           result.put(library.key, library);
         }
       }
 
-      roots.addAll(rule.dependencies);
+      for (Label dep : rule.dependencies) {
+        roots.add(RuleKey.forDependency(rule, dep));
+      }
     }
   }
 
@@ -399,52 +332,54 @@
       return;
     }
 
-    Label label = rule.label;
-    Collection<String> jars = jdepsMap.getDependenciesForRule(label);
+    RuleKey ruleKey = rule.key;
+    Collection<String> jars = jdepsMap.getDependenciesForRule(ruleKey);
     if (jars != null) {
       workspaceBuilder.jdeps.addAll(jars);
     }
 
     // Add all deps if this rule is in the current working set
     if (workingSet == null || workingSet.isRuleInWorkingSet(rule)) {
-      workspaceBuilder.directDeps.add(
-          label); // Add self, so we pick up our own gen jars if in working set
-      workspaceBuilder.directDeps.addAll(rule.dependencies);
+      // Add self, so we pick up our own gen jars if in working set
+      workspaceBuilder.directDeps.add(ruleKey);
+      for (Label dep : rule.dependencies) {
+        workspaceBuilder.directDeps.add(RuleKey.forDependency(rule, dep));
+      }
     }
 
     for (ArtifactLocation artifactLocation : javaSources) {
       if (artifactLocation.isSource()) {
-        duplicateSourceDetector.add(label, artifactLocation);
-        workspaceBuilder.sourceArtifacts.add(new SourceArtifact(label, artifactLocation));
-        workspaceBuilder.addedSourceFiles.add(artifactLocation.getFile());
+        duplicateSourceDetector.add(ruleKey, artifactLocation);
+        workspaceBuilder.sourceArtifacts.add(new SourceArtifact(ruleKey, artifactLocation));
+        workspaceBuilder.addedSourceFiles.add(artifactLocation);
       }
     }
 
     ArtifactLocation manifest = javaRuleIdeInfo.packageManifest;
     if (manifest != null) {
-      workspaceBuilder.javaPackageManifests.put(label, manifest);
+      workspaceBuilder.javaPackageManifests.put(ruleKey, manifest);
     }
     for (LibraryArtifact libraryArtifact : javaRuleIdeInfo.jars) {
       ArtifactLocation classJar = libraryArtifact.classJar;
       if (classJar != null) {
-        workspaceBuilder.buildOutputJars.add(classJar.getFile());
+        workspaceBuilder.buildOutputJars.add(classJar);
       }
     }
     workspaceBuilder.generatedJarsFromSourceRules.addAll(
         javaRuleIdeInfo
             .generatedJars
             .stream()
-            .map(libraryArtifact -> new BlazeJarLibrary(libraryArtifact, label))
+            .map(libraryArtifact -> new BlazeJarLibrary(libraryArtifact, ruleKey))
             .collect(Collectors.toList()));
     if (javaRuleIdeInfo.filteredGenJar != null) {
       workspaceBuilder.generatedJarsFromSourceRules.add(
-          new BlazeJarLibrary(javaRuleIdeInfo.filteredGenJar, label));
+          new BlazeJarLibrary(javaRuleIdeInfo.filteredGenJar, ruleKey));
     }
 
     for (BlazeJavaSyncAugmenter augmenter : augmenters) {
       augmenter.addJarsForSourceRule(
           rule,
-          workspaceBuilder.outputJarsFromSourceRules.get(label),
+          workspaceBuilder.outputJarsFromSourceRules.get(ruleKey),
           workspaceBuilder.generatedJarsFromSourceRules);
     }
   }
@@ -459,14 +394,14 @@
     return null;
   }
 
-  static class WorkspaceBuilder {
+  private static class WorkspaceBuilder {
     Set<String> jdeps = Sets.newHashSet();
-    Set<Label> directDeps = Sets.newHashSet();
-    Set<File> addedSourceFiles = Sets.newHashSet();
-    Multimap<Label, BlazeJarLibrary> outputJarsFromSourceRules = ArrayListMultimap.create();
+    Set<RuleKey> directDeps = Sets.newHashSet();
+    Set<ArtifactLocation> addedSourceFiles = Sets.newHashSet();
+    Multimap<RuleKey, BlazeJarLibrary> outputJarsFromSourceRules = ArrayListMultimap.create();
     List<BlazeJarLibrary> generatedJarsFromSourceRules = Lists.newArrayList();
-    List<File> buildOutputJars = Lists.newArrayList();
+    List<ArtifactLocation> buildOutputJars = Lists.newArrayList();
     List<SourceArtifact> sourceArtifacts = Lists.newArrayList();
-    Map<Label, ArtifactLocation> javaPackageManifests = Maps.newHashMap();
+    Map<RuleKey, ArtifactLocation> javaPackageManifests = Maps.newHashMap();
   }
 }
diff --git a/java/src/com/google/idea/blaze/java/sync/importer/JavaSourceFilter.java b/java/src/com/google/idea/blaze/java/sync/importer/JavaSourceFilter.java
new file mode 100644
index 0000000..0474836
--- /dev/null
+++ b/java/src/com/google/idea/blaze/java/sync/importer/JavaSourceFilter.java
@@ -0,0 +1,115 @@
+/*
+ * Copyright 2016 The Bazel Authors. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.idea.blaze.java.sync.importer;
+
+import com.google.common.collect.Lists;
+import com.google.common.collect.Maps;
+import com.google.idea.blaze.base.ideinfo.ArtifactLocation;
+import com.google.idea.blaze.base.ideinfo.RuleIdeInfo;
+import com.google.idea.blaze.base.ideinfo.RuleKey;
+import com.google.idea.blaze.base.ideinfo.RuleMap;
+import com.google.idea.blaze.base.model.primitives.Kind;
+import com.google.idea.blaze.base.model.primitives.WorkspaceRoot;
+import com.google.idea.blaze.base.projectview.ProjectViewSet;
+import com.google.idea.blaze.base.sync.projectview.ProjectViewRuleImportFilter;
+import com.google.idea.common.experiments.BoolExperiment;
+import com.intellij.openapi.project.Project;
+import java.util.Collection;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
+
+/** Segments java rules into source/libraries */
+public class JavaSourceFilter {
+  private static final BoolExperiment NO_EMPTY_SOURCE_RULES =
+      new BoolExperiment("no.empty.source.rules", true);
+
+  final List<RuleIdeInfo> sourceRules;
+  final List<RuleIdeInfo> libraryRules;
+  final List<RuleIdeInfo> protoLibraries;
+  final Map<RuleKey, Collection<ArtifactLocation>> ruleToJavaSources;
+
+  public JavaSourceFilter(
+      Project project,
+      WorkspaceRoot workspaceRoot,
+      ProjectViewSet projectViewSet,
+      RuleMap ruleMap) {
+    ProjectViewRuleImportFilter importFilter =
+        new ProjectViewRuleImportFilter(project, workspaceRoot, projectViewSet);
+    List<RuleIdeInfo> includedRules =
+        ruleMap
+            .rules()
+            .stream()
+            .filter(rule -> !importFilter.excludeTarget(rule))
+            .collect(Collectors.toList());
+
+    List<RuleIdeInfo> javaRules =
+        includedRules
+            .stream()
+            .filter(rule -> rule.javaRuleIdeInfo != null)
+            .collect(Collectors.toList());
+
+    ruleToJavaSources = Maps.newHashMap();
+    for (RuleIdeInfo rule : javaRules) {
+      List<ArtifactLocation> javaSources =
+          rule.sources
+              .stream()
+              .filter(source -> source.getRelativePath().endsWith(".java"))
+              .collect(Collectors.toList());
+      ruleToJavaSources.put(rule.key, javaSources);
+    }
+
+    boolean noEmptySourceRules = NO_EMPTY_SOURCE_RULES.getValue();
+    sourceRules = Lists.newArrayList();
+    libraryRules = Lists.newArrayList();
+    for (RuleIdeInfo rule : javaRules) {
+      boolean importAsSource =
+          importFilter.isSourceRule(rule)
+              && canImportAsSource(rule)
+              && (noEmptySourceRules
+                  ? anyNonGeneratedSources(ruleToJavaSources.get(rule.key))
+                  : !allSourcesGenerated(ruleToJavaSources.get(rule.key)));
+
+      if (importAsSource) {
+        sourceRules.add(rule);
+      } else {
+        libraryRules.add(rule);
+      }
+    }
+
+    protoLibraries =
+        includedRules
+            .stream()
+            .filter(rule -> rule.kind == Kind.PROTO_LIBRARY)
+            .collect(Collectors.toList());
+  }
+
+  public Iterable<RuleIdeInfo> getSourceRules() {
+    return sourceRules;
+  }
+
+  private boolean canImportAsSource(RuleIdeInfo rule) {
+    return !rule.kindIsOneOf(Kind.JAVA_WRAP_CC, Kind.JAVA_IMPORT);
+  }
+
+  private boolean allSourcesGenerated(Collection<ArtifactLocation> sources) {
+    return !sources.isEmpty() && sources.stream().allMatch(ArtifactLocation::isGenerated);
+  }
+
+  private boolean anyNonGeneratedSources(Collection<ArtifactLocation> sources) {
+    return sources.stream().anyMatch(ArtifactLocation::isSource);
+  }
+}
diff --git a/java/src/com/google/idea/blaze/java/sync/jdeps/JdepsFileReader.java b/java/src/com/google/idea/blaze/java/sync/jdeps/JdepsFileReader.java
index 4570965..c4bdaac 100644
--- a/java/src/com/google/idea/blaze/java/sync/jdeps/JdepsFileReader.java
+++ b/java/src/com/google/idea/blaze/java/sync/jdeps/JdepsFileReader.java
@@ -26,14 +26,14 @@
 import com.google.idea.blaze.base.ideinfo.ArtifactLocation;
 import com.google.idea.blaze.base.ideinfo.JavaRuleIdeInfo;
 import com.google.idea.blaze.base.ideinfo.RuleIdeInfo;
-import com.google.idea.blaze.base.model.RuleMap;
+import com.google.idea.blaze.base.ideinfo.RuleKey;
 import com.google.idea.blaze.base.model.SyncState;
-import com.google.idea.blaze.base.model.primitives.Label;
 import com.google.idea.blaze.base.prefetch.PrefetchService;
 import com.google.idea.blaze.base.scope.BlazeContext;
 import com.google.idea.blaze.base.scope.Scope;
 import com.google.idea.blaze.base.scope.output.PrintOutput;
 import com.google.idea.blaze.base.scope.scopes.TimingScope;
+import com.google.idea.blaze.base.sync.workspace.ArtifactLocationDecoder;
 import com.google.repackaged.devtools.build.lib.view.proto.Deps;
 import com.intellij.openapi.diagnostic.Logger;
 import com.intellij.openapi.project.Project;
@@ -54,20 +54,20 @@
   private static final Logger LOG = Logger.getInstance(JdepsFileReader.class);
 
   static class JdepsState implements Serializable {
-    private static final long serialVersionUID = 3L;
+    private static final long serialVersionUID = 4L;
     private ImmutableMap<File, Long> fileState = null;
-    private Map<File, Label> fileToLabelMap = Maps.newHashMap();
-    private Map<Label, List<String>> labelToJdeps = Maps.newHashMap();
+    private Map<File, RuleKey> fileToRuleMap = Maps.newHashMap();
+    private Map<RuleKey, List<String>> ruleToJdeps = Maps.newHashMap();
   }
 
   private static class Result {
     File file;
-    Label label;
+    RuleKey ruleKey;
     List<String> dependencies;
 
-    public Result(File file, Label label, List<String> dependencies) {
+    public Result(File file, RuleKey ruleKey, List<String> dependencies) {
       this.file = file;
-      this.label = label;
+      this.ruleKey = ruleKey;
       this.dependencies = dependencies;
     }
   }
@@ -77,7 +77,8 @@
   public JdepsMap loadJdepsFiles(
       Project project,
       BlazeContext parentContext,
-      RuleMap ruleMap,
+      ArtifactLocationDecoder artifactLocationDecoder,
+      Iterable<RuleIdeInfo> rulesToLoad,
       SyncState.Builder syncStateBuilder,
       @Nullable SyncState previousSyncState) {
     JdepsState oldState =
@@ -87,30 +88,36 @@
             parentContext,
             (context) -> {
               context.push(new TimingScope("LoadJdepsFiles"));
-              return doLoadJdepsFiles(project, context, oldState, ruleMap);
+              return doLoadJdepsFiles(
+                  project, context, artifactLocationDecoder, oldState, rulesToLoad);
             });
     if (jdepsState == null) {
       return null;
     }
     syncStateBuilder.put(JdepsState.class, jdepsState);
-    return label -> jdepsState.labelToJdeps.get(label);
+    return ruleKey -> jdepsState.ruleToJdeps.get(ruleKey);
   }
 
   private JdepsState doLoadJdepsFiles(
-      Project project, BlazeContext context, @Nullable JdepsState oldState, RuleMap ruleMap) {
+      Project project,
+      BlazeContext context,
+      ArtifactLocationDecoder artifactLocationDecoder,
+      @Nullable JdepsState oldState,
+      Iterable<RuleIdeInfo> rulesToLoad) {
     JdepsState state = new JdepsState();
     if (oldState != null) {
-      state.labelToJdeps = Maps.newHashMap(oldState.labelToJdeps);
-      state.fileToLabelMap = Maps.newHashMap(oldState.fileToLabelMap);
+      state.ruleToJdeps = Maps.newHashMap(oldState.ruleToJdeps);
+      state.fileToRuleMap = Maps.newHashMap(oldState.fileToRuleMap);
     }
 
-    List<File> files = Lists.newArrayList();
-    for (RuleIdeInfo ruleIdeInfo : ruleMap.rules()) {
+    Map<File, RuleKey> fileToRuleMap = Maps.newHashMap();
+    for (RuleIdeInfo ruleIdeInfo : rulesToLoad) {
+      assert ruleIdeInfo != null;
       JavaRuleIdeInfo javaRuleIdeInfo = ruleIdeInfo.javaRuleIdeInfo;
       if (javaRuleIdeInfo != null) {
         ArtifactLocation jdepsFile = javaRuleIdeInfo.jdepsFile;
         if (jdepsFile != null) {
-          files.add(jdepsFile.getFile());
+          fileToRuleMap.put(artifactLocationDecoder.decode(jdepsFile), ruleIdeInfo.key);
         }
       }
     }
@@ -119,7 +126,10 @@
     List<File> removedFiles = Lists.newArrayList();
     state.fileState =
         FileDiffer.updateFiles(
-            oldState != null ? oldState.fileState : null, files, updatedFiles, removedFiles);
+            oldState != null ? oldState.fileState : null,
+            fileToRuleMap.keySet(),
+            updatedFiles,
+            removedFiles);
 
     ListenableFuture<?> fetchFuture =
         PrefetchService.getInstance().prefetchFiles(project, updatedFiles);
@@ -132,9 +142,9 @@
     }
 
     for (File removedFile : removedFiles) {
-      Label label = state.fileToLabelMap.remove(removedFile);
-      if (label != null) {
-        state.labelToJdeps.remove(label);
+      RuleKey ruleKey = state.fileToRuleMap.remove(removedFile);
+      if (ruleKey != null) {
+        state.ruleToJdeps.remove(ruleKey);
       }
     }
 
@@ -149,20 +159,18 @@
                 try (InputStream inputStream = new FileInputStream(updatedFile)) {
                   Deps.Dependencies dependencies = Deps.Dependencies.parseFrom(inputStream);
                   if (dependencies != null) {
-                    if (dependencies.hasRuleLabel()) {
-                      Label label = new Label(dependencies.getRuleLabel());
-                      List<String> dependencyStringList = Lists.newArrayList();
-                      for (Deps.Dependency dependency : dependencies.getDependencyList()) {
-                        // We only want explicit or implicit deps that were
-                        // actually resolved by the compiler, not ones that are
-                        // available for use in the same package
-                        if (dependency.getKind() == Deps.Dependency.Kind.EXPLICIT
-                            || dependency.getKind() == Deps.Dependency.Kind.IMPLICIT) {
-                          dependencyStringList.add(dependency.getPath());
-                        }
+                    List<String> dependencyStringList = Lists.newArrayList();
+                    for (Deps.Dependency dependency : dependencies.getDependencyList()) {
+                      // We only want explicit or implicit deps that were
+                      // actually resolved by the compiler, not ones that are
+                      // available for use in the same package
+                      if (dependency.getKind() == Deps.Dependency.Kind.EXPLICIT
+                          || dependency.getKind() == Deps.Dependency.Kind.IMPLICIT) {
+                        dependencyStringList.add(dependency.getPath());
                       }
-                      return new Result(updatedFile, label, dependencyStringList);
                     }
+                    RuleKey ruleKey = fileToRuleMap.get(updatedFile);
+                    return new Result(updatedFile, ruleKey, dependencyStringList);
                   }
                 } catch (FileNotFoundException e) {
                   LOG.info("Could not open jdeps file: " + updatedFile);
@@ -173,8 +181,8 @@
     try {
       for (Result result : Futures.allAsList(futures).get()) {
         if (result != null) {
-          state.fileToLabelMap.put(result.file, result.label);
-          state.labelToJdeps.put(result.label, result.dependencies);
+          state.fileToRuleMap.put(result.file, result.ruleKey);
+          state.ruleToJdeps.put(result.ruleKey, result.dependencies);
         }
       }
       context.output(
diff --git a/java/src/com/google/idea/blaze/java/sync/jdeps/JdepsMap.java b/java/src/com/google/idea/blaze/java/sync/jdeps/JdepsMap.java
index 73ae6a2..32b5452 100644
--- a/java/src/com/google/idea/blaze/java/sync/jdeps/JdepsMap.java
+++ b/java/src/com/google/idea/blaze/java/sync/jdeps/JdepsMap.java
@@ -15,9 +15,8 @@
  */
 package com.google.idea.blaze.java.sync.jdeps;
 
-import com.google.idea.blaze.base.model.primitives.Label;
+import com.google.idea.blaze.base.ideinfo.RuleKey;
 import java.util.List;
-import org.jetbrains.annotations.NotNull;
 import org.jetbrains.annotations.Nullable;
 
 /** Map of rule -> jdeps dependencies. */
@@ -31,5 +30,5 @@
    * <p>If the rule doesn't have source or otherwise wasn't instrumented, null is returned.
    */
   @Nullable
-  List<String> getDependenciesForRule(@NotNull Label label);
+  List<String> getDependenciesForRule(RuleKey ruleKey);
 }
diff --git a/java/src/com/google/idea/blaze/java/sync/model/BlazeJarLibrary.java b/java/src/com/google/idea/blaze/java/sync/model/BlazeJarLibrary.java
index 2470e63..02bee44 100644
--- a/java/src/com/google/idea/blaze/java/sync/model/BlazeJarLibrary.java
+++ b/java/src/com/google/idea/blaze/java/sync/model/BlazeJarLibrary.java
@@ -17,7 +17,8 @@
 
 import com.google.common.base.Objects;
 import com.google.idea.blaze.base.ideinfo.LibraryArtifact;
-import com.google.idea.blaze.base.model.primitives.Label;
+import com.google.idea.blaze.base.ideinfo.RuleKey;
+import com.google.idea.blaze.base.sync.workspace.ArtifactLocationDecoder;
 import com.google.idea.blaze.java.libraries.JarCache;
 import com.google.idea.blaze.java.libraries.SourceJarManager;
 import com.google.idea.blaze.java.settings.BlazeJavaUserSettings;
@@ -30,22 +31,25 @@
 /** An immutable reference to a .jar required by a rule. */
 @Immutable
 public final class BlazeJarLibrary extends BlazeLibrary {
-  private static final long serialVersionUID = 1L;
+  private static final long serialVersionUID = 2L;
 
   public final LibraryArtifact libraryArtifact;
 
-  public final Label originatingRule;
+  public final RuleKey originatingRule;
 
-  public BlazeJarLibrary(LibraryArtifact libraryArtifact, Label originatingRule) {
-    super(LibraryKey.fromJarFile(libraryArtifact.jarForIntellijLibrary().getFile()));
+  public BlazeJarLibrary(LibraryArtifact libraryArtifact, RuleKey originatingRule) {
+    super(LibraryKey.fromJarFile(libraryArtifact.jarForIntellijLibrary()));
     this.libraryArtifact = libraryArtifact;
     this.originatingRule = originatingRule;
   }
 
   @Override
-  public void modifyLibraryModel(Project project, Library.ModifiableModel libraryModel) {
+  public void modifyLibraryModel(
+      Project project,
+      ArtifactLocationDecoder artifactLocationDecoder,
+      Library.ModifiableModel libraryModel) {
     JarCache jarCache = JarCache.getInstance(project);
-    File jar = jarCache.getCachedJar(this);
+    File jar = jarCache.getCachedJar(artifactLocationDecoder, this);
     libraryModel.addRoot(pathToUrl(jar), OrderRootType.CLASSES);
 
     boolean attachSourcesByDefault =
@@ -53,7 +57,7 @@
     SourceJarManager sourceJarManager = SourceJarManager.getInstance(project);
     boolean attachSourceJar = attachSourcesByDefault || sourceJarManager.hasSourceJarAttached(key);
     if (attachSourceJar && libraryArtifact.sourceJar != null) {
-      File sourceJar = jarCache.getCachedSourceJar(this);
+      File sourceJar = jarCache.getCachedSourceJar(artifactLocationDecoder, this);
       if (sourceJar != null) {
         libraryModel.addRoot(pathToUrl(sourceJar), OrderRootType.SOURCES);
       }
diff --git a/java/src/com/google/idea/blaze/java/sync/model/BlazeJavaImportResult.java b/java/src/com/google/idea/blaze/java/sync/model/BlazeJavaImportResult.java
index f033772..eb3c3c8 100644
--- a/java/src/com/google/idea/blaze/java/sync/model/BlazeJavaImportResult.java
+++ b/java/src/com/google/idea/blaze/java/sync/model/BlazeJavaImportResult.java
@@ -19,7 +19,7 @@
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableMap;
 import com.google.common.collect.ImmutableSet;
-import java.io.File;
+import com.google.idea.blaze.base.ideinfo.ArtifactLocation;
 import java.io.Serializable;
 import javax.annotation.Nullable;
 import javax.annotation.concurrent.Immutable;
@@ -27,19 +27,19 @@
 /** The result of a blaze import operation. */
 @Immutable
 public class BlazeJavaImportResult implements Serializable {
-  private static final long serialVersionUID = 3L;
+  private static final long serialVersionUID = 4L;
 
   public final ImmutableList<BlazeContentEntry> contentEntries;
   public final ImmutableMap<LibraryKey, BlazeJarLibrary> libraries;
-  public final ImmutableCollection<File> buildOutputJars;
-  public final ImmutableSet<File> javaSourceFiles;
+  public final ImmutableCollection<ArtifactLocation> buildOutputJars;
+  public final ImmutableSet<ArtifactLocation> javaSourceFiles;
   @Nullable public final String sourceVersion;
 
   public BlazeJavaImportResult(
       ImmutableList<BlazeContentEntry> contentEntries,
       ImmutableMap<LibraryKey, BlazeJarLibrary> libraries,
-      ImmutableCollection<File> buildOutputJars,
-      ImmutableSet<File> javaSourceFiles,
+      ImmutableCollection<ArtifactLocation> buildOutputJars,
+      ImmutableSet<ArtifactLocation> javaSourceFiles,
       @Nullable String sourceVersion) {
     this.contentEntries = contentEntries;
     this.libraries = libraries;
diff --git a/java/src/com/google/idea/blaze/java/sync/model/BlazeLibrary.java b/java/src/com/google/idea/blaze/java/sync/model/BlazeLibrary.java
index f84fc4c..f289717 100644
--- a/java/src/com/google/idea/blaze/java/sync/model/BlazeLibrary.java
+++ b/java/src/com/google/idea/blaze/java/sync/model/BlazeLibrary.java
@@ -16,6 +16,7 @@
 package com.google.idea.blaze.java.sync.model;
 
 import com.google.common.base.Objects;
+import com.google.idea.blaze.base.sync.workspace.ArtifactLocationDecoder;
 import com.intellij.openapi.project.Project;
 import com.intellij.openapi.roots.libraries.Library;
 import com.intellij.openapi.util.io.FileUtil;
@@ -61,7 +62,10 @@
     return Objects.equal(key, that.key);
   }
 
-  public abstract void modifyLibraryModel(Project project, Library.ModifiableModel libraryModel);
+  public abstract void modifyLibraryModel(
+      Project project,
+      ArtifactLocationDecoder artifactLocationDecoder,
+      Library.ModifiableModel libraryModel);
 
   protected static String pathToUrl(File path) {
     String name = path.getName();
diff --git a/java/src/com/google/idea/blaze/java/sync/model/LibraryKey.java b/java/src/com/google/idea/blaze/java/sync/model/LibraryKey.java
index 8496a31..fe19973 100644
--- a/java/src/com/google/idea/blaze/java/sync/model/LibraryKey.java
+++ b/java/src/com/google/idea/blaze/java/sync/model/LibraryKey.java
@@ -15,34 +15,33 @@
  */
 package com.google.idea.blaze.java.sync.model;
 
+import com.google.idea.blaze.base.ideinfo.ArtifactLocation;
 import com.intellij.openapi.roots.libraries.Library;
 import com.intellij.openapi.util.io.FileUtil;
 import java.io.File;
 import java.io.Serializable;
 import javax.annotation.concurrent.Immutable;
-import org.jetbrains.annotations.NotNull;
 
 /** Uniquely identifies a library as imported into IntellJ. */
 @Immutable
 public final class LibraryKey implements Serializable {
   public static final long serialVersionUID = 1L;
 
-  @NotNull private final String name;
+  private final String name;
 
-  @NotNull
-  public static LibraryKey fromJarFile(@NotNull File jarFile) {
-    int parentHash = jarFile.getParent().hashCode();
+  public static LibraryKey fromJarFile(ArtifactLocation artifactLocation) {
+    File jarFile = new File(artifactLocation.getRelativePath());
+    String parent = jarFile.getParent();
+    int parentHash = parent != null ? parent.hashCode() : jarFile.hashCode();
     String name = FileUtil.getNameWithoutExtension(jarFile) + "_" + Integer.toHexString(parentHash);
     return new LibraryKey(name);
   }
 
-  @NotNull
   public static LibraryKey forResourceLibrary() {
     return new LibraryKey("external_resources_library");
   }
 
-  @NotNull
-  public static LibraryKey fromIntelliJLibrary(@NotNull Library library) {
+  public static LibraryKey fromIntelliJLibrary(Library library) {
     String name = library.getName();
     if (name == null) {
       throw new IllegalArgumentException("Null library name");
@@ -50,12 +49,11 @@
     return fromIntelliJLibraryName(name);
   }
 
-  @NotNull
-  public static LibraryKey fromIntelliJLibraryName(@NotNull String name) {
+  public static LibraryKey fromIntelliJLibraryName(String name) {
     return new LibraryKey(name);
   }
 
-  LibraryKey(@NotNull String name) {
+  LibraryKey(String name) {
     this.name = name;
   }
 
diff --git a/java/src/com/google/idea/blaze/java/sync/projectstructure/LibraryEditor.java b/java/src/com/google/idea/blaze/java/sync/projectstructure/LibraryEditor.java
index ea197ed..aa6ba5e 100644
--- a/java/src/com/google/idea/blaze/java/sync/projectstructure/LibraryEditor.java
+++ b/java/src/com/google/idea/blaze/java/sync/projectstructure/LibraryEditor.java
@@ -19,6 +19,7 @@
 import com.google.idea.blaze.base.model.BlazeProjectData;
 import com.google.idea.blaze.base.scope.BlazeContext;
 import com.google.idea.blaze.base.scope.output.PrintOutput;
+import com.google.idea.blaze.base.sync.workspace.ArtifactLocationDecoder;
 import com.google.idea.blaze.java.sync.BlazeJavaSyncAugmenter;
 import com.google.idea.blaze.java.sync.model.BlazeLibrary;
 import com.google.idea.blaze.java.sync.model.LibraryKey;
@@ -62,7 +63,12 @@
     LibraryTable.ModifiableModel libraryTableModel = libraryTable.getModifiableModel();
     try {
       for (BlazeLibrary library : libraries) {
-        updateLibrary(project, libraryTable, libraryTableModel, library);
+        updateLibrary(
+            project,
+            blazeProjectData.artifactLocationDecoder,
+            libraryTable,
+            libraryTableModel,
+            library);
       }
 
       // Garbage collect unused libraries
@@ -85,6 +91,7 @@
 
   public static void updateLibrary(
       Project project,
+      ArtifactLocationDecoder artifactLocationDecoder,
       LibraryTable libraryTable,
       LibraryTable.ModifiableModel libraryTableModel,
       BlazeLibrary blazeLibrary) {
@@ -105,7 +112,7 @@
       }
     }
     try {
-      blazeLibrary.modifyLibraryModel(project, libraryModel);
+      blazeLibrary.modifyLibraryModel(project, artifactLocationDecoder, libraryModel);
     } finally {
       libraryModel.commit();
     }
diff --git a/java/src/com/google/idea/blaze/java/sync/source/FilePathJavaPackageReader.java b/java/src/com/google/idea/blaze/java/sync/source/FilePathJavaPackageReader.java
index 99e3c66..0c1265a 100644
--- a/java/src/com/google/idea/blaze/java/sync/source/FilePathJavaPackageReader.java
+++ b/java/src/com/google/idea/blaze/java/sync/source/FilePathJavaPackageReader.java
@@ -17,12 +17,16 @@
 
 import com.google.idea.blaze.base.model.primitives.WorkspacePath;
 import com.google.idea.blaze.base.scope.BlazeContext;
+import com.google.idea.blaze.base.sync.workspace.ArtifactLocationDecoder;
 import com.google.idea.blaze.base.util.PackagePrefixCalculator;
 
 /** Gets the package from a java file by its file path alone (i.e. without opening the file). */
 public final class FilePathJavaPackageReader extends JavaPackageReader {
   @Override
-  public String getDeclaredPackageOfJavaFile(BlazeContext context, SourceArtifact sourceArtifact) {
+  public String getDeclaredPackageOfJavaFile(
+      BlazeContext context,
+      ArtifactLocationDecoder artifactLocationDecoder,
+      SourceArtifact sourceArtifact) {
     String directory = sourceArtifact.artifactLocation.getRelativePath();
     int i = directory.lastIndexOf('/');
     if (i >= 0) {
diff --git a/java/src/com/google/idea/blaze/java/sync/source/JavaPackageReader.java b/java/src/com/google/idea/blaze/java/sync/source/JavaPackageReader.java
index 9c1f037..ee190e9 100644
--- a/java/src/com/google/idea/blaze/java/sync/source/JavaPackageReader.java
+++ b/java/src/com/google/idea/blaze/java/sync/source/JavaPackageReader.java
@@ -16,10 +16,14 @@
 package com.google.idea.blaze.java.sync.source;
 
 import com.google.idea.blaze.base.scope.BlazeContext;
+import com.google.idea.blaze.base.sync.workspace.ArtifactLocationDecoder;
 import javax.annotation.Nullable;
 
 /** Reads java packages from files. */
 public abstract class JavaPackageReader {
   @Nullable
-  abstract String getDeclaredPackageOfJavaFile(BlazeContext context, SourceArtifact sourceArtifact);
+  abstract String getDeclaredPackageOfJavaFile(
+      BlazeContext context,
+      ArtifactLocationDecoder artifactLocationDecoder,
+      SourceArtifact sourceArtifact);
 }
diff --git a/java/src/com/google/idea/blaze/java/sync/source/JavaSourcePackageReader.java b/java/src/com/google/idea/blaze/java/sync/source/JavaSourcePackageReader.java
index 0f3eca0..f67f76c 100644
--- a/java/src/com/google/idea/blaze/java/sync/source/JavaSourcePackageReader.java
+++ b/java/src/com/google/idea/blaze/java/sync/source/JavaSourcePackageReader.java
@@ -15,9 +15,12 @@
  */
 package com.google.idea.blaze.java.sync.source;
 
+import static java.nio.charset.StandardCharsets.UTF_8;
+
 import com.google.idea.blaze.base.io.InputStreamProvider;
 import com.google.idea.blaze.base.scope.BlazeContext;
 import com.google.idea.blaze.base.scope.output.IssueOutput;
+import com.google.idea.blaze.base.sync.workspace.ArtifactLocationDecoder;
 import com.intellij.openapi.components.ServiceManager;
 import com.intellij.openapi.diagnostic.Logger;
 import java.io.BufferedReader;
@@ -44,14 +47,17 @@
 
   @Override
   @Nullable
-  public String getDeclaredPackageOfJavaFile(BlazeContext context, SourceArtifact sourceArtifact) {
+  public String getDeclaredPackageOfJavaFile(
+      BlazeContext context,
+      ArtifactLocationDecoder artifactLocationDecoder,
+      SourceArtifact sourceArtifact) {
     if (sourceArtifact.artifactLocation.isGenerated()) {
       return null;
     }
     InputStreamProvider inputStreamProvider = InputStreamProvider.getInstance();
-    File sourceFile = sourceArtifact.artifactLocation.getFile();
+    File sourceFile = artifactLocationDecoder.decode(sourceArtifact.artifactLocation);
     try (InputStream javaInputStream = inputStreamProvider.getFile(sourceFile)) {
-      BufferedReader javaReader = new BufferedReader(new InputStreamReader(javaInputStream));
+      BufferedReader javaReader = new BufferedReader(new InputStreamReader(javaInputStream, UTF_8));
       String javaLine;
 
       while ((javaLine = javaReader.readLine()) != null) {
diff --git a/java/src/com/google/idea/blaze/java/sync/source/ManifestFilePackageReader.java b/java/src/com/google/idea/blaze/java/sync/source/ManifestFilePackageReader.java
index 726d60a..96d1177 100644
--- a/java/src/com/google/idea/blaze/java/sync/source/ManifestFilePackageReader.java
+++ b/java/src/com/google/idea/blaze/java/sync/source/ManifestFilePackageReader.java
@@ -15,25 +15,31 @@
  */
 package com.google.idea.blaze.java.sync.source;
 
-import com.google.idea.blaze.base.model.primitives.Label;
+import com.google.idea.blaze.base.ideinfo.ArtifactLocation;
+import com.google.idea.blaze.base.ideinfo.RuleKey;
 import com.google.idea.blaze.base.scope.BlazeContext;
+import com.google.idea.blaze.base.sync.workspace.ArtifactLocationDecoder;
 import java.util.Map;
 import javax.annotation.Nullable;
 
 class ManifestFilePackageReader extends JavaPackageReader {
 
-  private final Map<Label, Map<String, String>> manifestMap;
+  private final Map<RuleKey, Map<ArtifactLocation, String>> manifestMap;
 
-  public ManifestFilePackageReader(Map<Label, Map<String, String>> manifestMap) {
+  public ManifestFilePackageReader(Map<RuleKey, Map<ArtifactLocation, String>> manifestMap) {
     this.manifestMap = manifestMap;
   }
 
   @Nullable
   @Override
-  String getDeclaredPackageOfJavaFile(BlazeContext context, SourceArtifact sourceArtifact) {
-    Map<String, String> manifestMapForRule = manifestMap.get(sourceArtifact.originatingRule);
+  String getDeclaredPackageOfJavaFile(
+      BlazeContext context,
+      ArtifactLocationDecoder artifactLocationDecoder,
+      SourceArtifact sourceArtifact) {
+    Map<ArtifactLocation, String> manifestMapForRule =
+        manifestMap.get(sourceArtifact.originatingRule);
     if (manifestMapForRule != null) {
-      return manifestMapForRule.get(sourceArtifact.artifactLocation.getFile().getPath());
+      return manifestMapForRule.get(sourceArtifact.artifactLocation);
     }
     return null;
   }
diff --git a/java/src/com/google/idea/blaze/java/sync/source/PackageManifestReader.java b/java/src/com/google/idea/blaze/java/sync/source/PackageManifestReader.java
index 61ec202..96a0813 100644
--- a/java/src/com/google/idea/blaze/java/sync/source/PackageManifestReader.java
+++ b/java/src/com/google/idea/blaze/java/sync/source/PackageManifestReader.java
@@ -24,8 +24,8 @@
 import com.google.idea.blaze.base.async.FutureUtil;
 import com.google.idea.blaze.base.filecache.FileDiffer;
 import com.google.idea.blaze.base.ideinfo.ArtifactLocation;
+import com.google.idea.blaze.base.ideinfo.RuleKey;
 import com.google.idea.blaze.base.io.InputStreamProvider;
-import com.google.idea.blaze.base.model.primitives.Label;
 import com.google.idea.blaze.base.prefetch.PrefetchService;
 import com.google.idea.blaze.base.scope.BlazeContext;
 import com.google.idea.blaze.base.sync.workspace.ArtifactLocationDecoder;
@@ -41,7 +41,6 @@
 import java.util.List;
 import java.util.Map;
 import java.util.concurrent.ExecutionException;
-import javax.annotation.Nullable;
 
 /** Reads package manifests. */
 public class PackageManifestReader {
@@ -53,22 +52,22 @@
 
   private ImmutableMap<File, Long> fileDiffState;
 
-  private Map<File, Label> fileToLabelMap = Maps.newHashMap();
-  private final Map<Label, Map<String, String>> manifestMap = Maps.newConcurrentMap();
+  private Map<File, RuleKey> fileToLabelMap = Maps.newHashMap();
+  private final Map<RuleKey, Map<ArtifactLocation, String>> manifestMap = Maps.newConcurrentMap();
 
   /** @return A map from java source absolute file path to declared package string. */
-  public Map<Label, Map<String, String>> readPackageManifestFiles(
+  public Map<RuleKey, Map<ArtifactLocation, String>> readPackageManifestFiles(
       Project project,
       BlazeContext context,
       ArtifactLocationDecoder decoder,
-      Map<Label, ArtifactLocation> javaPackageManifests,
+      Map<RuleKey, ArtifactLocation> javaPackageManifests,
       ListeningExecutorService executorService) {
 
-    Map<File, Label> fileToLabelMap = Maps.newHashMap();
-    for (Map.Entry<Label, ArtifactLocation> entry : javaPackageManifests.entrySet()) {
-      Label label = entry.getKey();
-      File file = entry.getValue().getFile();
-      fileToLabelMap.put(file, label);
+    Map<File, RuleKey> fileToLabelMap = Maps.newHashMap();
+    for (Map.Entry<RuleKey, ArtifactLocation> entry : javaPackageManifests.entrySet()) {
+      RuleKey key = entry.getKey();
+      File file = decoder.decode(entry.getValue());
+      fileToLabelMap.put(file, key);
     }
     List<File> updatedFiles = Lists.newArrayList();
     List<File> removedFiles = Lists.newArrayList();
@@ -90,15 +89,15 @@
       futures.add(
           executorService.submit(
               () -> {
-                Map<String, String> manifest = parseManifestFile(decoder, file);
+                Map<ArtifactLocation, String> manifest = parseManifestFile(file);
                 manifestMap.put(fileToLabelMap.get(file), manifest);
                 return null;
               }));
     }
     for (File file : removedFiles) {
-      Label label = this.fileToLabelMap.get(file);
-      if (label != null) {
-        manifestMap.remove(label);
+      RuleKey key = this.fileToLabelMap.get(file);
+      if (key != null) {
+        manifestMap.remove(key);
       }
     }
     this.fileToLabelMap = fileToLabelMap;
@@ -112,19 +111,22 @@
     return manifestMap;
   }
 
-  protected Map<String, String> parseManifestFile(
-      ArtifactLocationDecoder decoder, File packageManifest) {
-    Map<String, String> outputMap = Maps.newHashMap();
+  protected Map<ArtifactLocation, String> parseManifestFile(File packageManifest) {
+    Map<ArtifactLocation, String> outputMap = Maps.newHashMap();
     InputStreamProvider inputStreamProvider = InputStreamProvider.getInstance();
 
     try (InputStream input = inputStreamProvider.getFile(packageManifest)) {
       try (BufferedInputStream bufferedInputStream = new BufferedInputStream(input)) {
         PackageManifest proto = PackageManifest.parseFrom(bufferedInputStream);
         for (JavaSourcePackage source : proto.getSourcesList()) {
-          String absPath = getAbsolutePath(decoder, source);
-          if (absPath != null) {
-            outputMap.put(absPath, source.getPackageString());
-          }
+          ArtifactLocation artifactLocation =
+              ArtifactLocation.builder()
+                  .setRootExecutionPathFragment(
+                      source.getArtifactLocation().getRootExecutionPathFragment())
+                  .setRelativePath(source.getArtifactLocation().getRelativePath())
+                  .setIsSource(source.getArtifactLocation().getIsSource())
+                  .build();
+          outputMap.put(artifactLocation, source.getPackageString());
         }
       }
       return outputMap;
@@ -133,17 +135,4 @@
       return outputMap;
     }
   }
-
-  /**
-   * Returns null if the artifact location file can't be found, presumably because it's been removed
-   * from the file system since the blaze build.
-   */
-  @Nullable
-  private static String getAbsolutePath(ArtifactLocationDecoder decoder, JavaSourcePackage source) {
-    ArtifactLocation location = decoder.decode(source.getArtifactLocation());
-    if (location == null) {
-      return null;
-    }
-    return location.getFile().getAbsolutePath();
-  }
 }
diff --git a/java/src/com/google/idea/blaze/java/sync/source/SourceArtifact.java b/java/src/com/google/idea/blaze/java/sync/source/SourceArtifact.java
index 37e35bd..6c00f09 100644
--- a/java/src/com/google/idea/blaze/java/sync/source/SourceArtifact.java
+++ b/java/src/com/google/idea/blaze/java/sync/source/SourceArtifact.java
@@ -16,27 +16,27 @@
 package com.google.idea.blaze.java.sync.source;
 
 import com.google.idea.blaze.base.ideinfo.ArtifactLocation;
-import com.google.idea.blaze.base.model.primitives.Label;
+import com.google.idea.blaze.base.ideinfo.RuleKey;
 
 /** Pairing of rule and source artifact. */
 public class SourceArtifact {
-  public final Label originatingRule;
+  public final RuleKey originatingRule;
   public final ArtifactLocation artifactLocation;
 
-  public SourceArtifact(Label originatingRule, ArtifactLocation artifactLocation) {
+  public SourceArtifact(RuleKey originatingRule, ArtifactLocation artifactLocation) {
     this.originatingRule = originatingRule;
     this.artifactLocation = artifactLocation;
   }
 
-  public static Builder builder(Label originatingRule) {
+  public static Builder builder(RuleKey originatingRule) {
     return new Builder(originatingRule);
   }
 
   static class Builder {
-    Label originatingRule;
+    RuleKey originatingRule;
     ArtifactLocation artifactLocation;
 
-    Builder(Label originatingRule) {
+    Builder(RuleKey originatingRule) {
       this.originatingRule = originatingRule;
     }
 
diff --git a/java/src/com/google/idea/blaze/java/sync/source/SourceDirectoryCalculator.java b/java/src/com/google/idea/blaze/java/sync/source/SourceDirectoryCalculator.java
index c4d992c..2fc3b21 100644
--- a/java/src/com/google/idea/blaze/java/sync/source/SourceDirectoryCalculator.java
+++ b/java/src/com/google/idea/blaze/java/sync/source/SourceDirectoryCalculator.java
@@ -34,7 +34,7 @@
 import com.google.common.util.concurrent.MoreExecutors;
 import com.google.idea.blaze.base.async.executor.TransientExecutor;
 import com.google.idea.blaze.base.ideinfo.ArtifactLocation;
-import com.google.idea.blaze.base.model.primitives.Label;
+import com.google.idea.blaze.base.ideinfo.RuleKey;
 import com.google.idea.blaze.base.model.primitives.WorkspacePath;
 import com.google.idea.blaze.base.model.primitives.WorkspaceRoot;
 import com.google.idea.blaze.base.scope.BlazeContext;
@@ -85,14 +85,14 @@
       ArtifactLocationDecoder artifactLocationDecoder,
       Collection<WorkspacePath> rootDirectories,
       Collection<SourceArtifact> sources,
-      Map<Label, ArtifactLocation> javaPackageManifests) {
+      Map<RuleKey, ArtifactLocation> javaPackageManifests) {
 
     ManifestFilePackageReader manifestFilePackageReader =
         Scope.push(
             context,
             (childContext) -> {
               childContext.push(new TimingScope("ReadPackageManifests"));
-              Map<Label, Map<String, String>> manifestMap =
+              Map<RuleKey, Map<ArtifactLocation, String>> manifestMap =
                   PackageManifestReader.getInstance()
                       .readPackageManifestFiles(
                           project,
@@ -125,8 +125,9 @@
             ImmutableList<BlazeSourceDirectory> sourceDirectories =
                 calculateSourceDirectoriesForContentRoot(
                     context,
-                    sourceTestConfig,
                     workspaceRoot,
+                    artifactLocationDecoder,
+                    sourceTestConfig,
                     workspacePath,
                     sourcesUnderDirectoryRoot.get(workspacePath),
                     javaPackageReaders);
@@ -168,13 +169,13 @@
       if (foundWorkspacePath != null) {
         result.put(foundWorkspacePath, sourceArtifact);
       } else if (sourceArtifact.artifactLocation.isSource()) {
-        File sourceFile = sourceArtifact.artifactLocation.getFile();
+        ArtifactLocation sourceFile = sourceArtifact.artifactLocation;
         String message =
             String.format(
                 "Did not add %s. You're probably using a java file from outside the workspace"
                     + " that has been exported using export_files. Don't do that.",
                 sourceFile);
-        IssueOutput.warn(message).inFile(sourceFile).submit(context);
+        IssueOutput.warn(message).submit(context);
       }
     }
     return result;
@@ -193,8 +194,9 @@
   /** Calculates all source directories for a single content root. */
   private ImmutableList<BlazeSourceDirectory> calculateSourceDirectoriesForContentRoot(
       BlazeContext context,
-      SourceTestConfig sourceTestConfig,
       WorkspaceRoot workspaceRoot,
+      ArtifactLocationDecoder artifactLocationDecoder,
+      SourceTestConfig sourceTestConfig,
       WorkspacePath directoryRoot,
       Collection<SourceArtifact> sourceArtifacts,
       Collection<JavaPackageReader> javaPackageReaders) {
@@ -213,6 +215,7 @@
     calculateJavaSourceDirectories(
         context,
         workspaceRoot,
+        artifactLocationDecoder,
         directoryRoot,
         sourceTestConfig,
         javaArtifacts,
@@ -227,6 +230,7 @@
   private void calculateJavaSourceDirectories(
       BlazeContext context,
       WorkspaceRoot workspaceRoot,
+      ArtifactLocationDecoder artifactLocationDecoder,
       WorkspacePath directoryRoot,
       SourceTestConfig sourceTestConfig,
       Collection<SourceArtifact> javaArtifacts,
@@ -240,7 +244,9 @@
     for (final SourceArtifact sourceArtifact : javaArtifacts) {
       ListenableFuture<SourceRoot> future =
           executorService.submit(
-              () -> sourceRootForJavaSource(context, sourceArtifact, javaPackageReaders));
+              () ->
+                  sourceRootForJavaSource(
+                      context, artifactLocationDecoder, sourceArtifact, javaPackageReaders));
       sourceRootFutures.add(future);
     }
     try {
@@ -443,23 +449,25 @@
   }
 
   @Nullable
-  private static SourceRoot sourceRootForJavaSource(
+  private SourceRoot sourceRootForJavaSource(
       BlazeContext context,
+      ArtifactLocationDecoder artifactLocationDecoder,
       SourceArtifact sourceArtifact,
       Collection<JavaPackageReader> javaPackageReaders) {
 
-    File javaFile = sourceArtifact.artifactLocation.getFile();
-
     String declaredPackage = null;
     for (JavaPackageReader reader : javaPackageReaders) {
-      declaredPackage = reader.getDeclaredPackageOfJavaFile(context, sourceArtifact);
+      declaredPackage =
+          reader.getDeclaredPackageOfJavaFile(context, artifactLocationDecoder, sourceArtifact);
       if (declaredPackage != null) {
         break;
       }
     }
     if (declaredPackage == null) {
-      IssueOutput.warn("Failed to inspect the package name of java source file: " + javaFile)
-          .inFile(javaFile)
+      IssueOutput.warn(
+              "Failed to inspect the package name of java source file: "
+                  + sourceArtifact.artifactLocation)
+          .inFile(artifactLocationDecoder.decode(sourceArtifact.artifactLocation))
           .submit(context);
       return null;
     }
diff --git a/java/src/com/google/idea/blaze/java/syncstatus/BlazeJavaSyncStatusClassNodeDecorator.java b/java/src/com/google/idea/blaze/java/syncstatus/BlazeJavaSyncStatusClassNodeDecorator.java
index 6ffabc2..01d7f40 100644
--- a/java/src/com/google/idea/blaze/java/syncstatus/BlazeJavaSyncStatusClassNodeDecorator.java
+++ b/java/src/com/google/idea/blaze/java/syncstatus/BlazeJavaSyncStatusClassNodeDecorator.java
@@ -48,7 +48,7 @@
     }
 
     Project project = node.getProject();
-    if (SyncStatusHelper.isUnsynced(project, virtualFile)) {
+    if (SyncStatusHelper.getInstance(project).isUnsynced(virtualFile)) {
       data.clearText();
       data.addText(psiClass.getName(), SimpleTextAttributes.GRAY_ATTRIBUTES);
       data.addText(" (unsynced)", SimpleTextAttributes.GRAY_ATTRIBUTES);
diff --git a/java/src/com/google/idea/blaze/java/syncstatus/BlazeJavaSyncStatusEditorTabColorProvider.java b/java/src/com/google/idea/blaze/java/syncstatus/BlazeJavaSyncStatusEditorTabColorProvider.java
index ad8e396..91234b1 100644
--- a/java/src/com/google/idea/blaze/java/syncstatus/BlazeJavaSyncStatusEditorTabColorProvider.java
+++ b/java/src/com/google/idea/blaze/java/syncstatus/BlazeJavaSyncStatusEditorTabColorProvider.java
@@ -31,7 +31,8 @@
   @Nullable
   @Override
   public Color getEditorTabColor(@NotNull Project project, @NotNull VirtualFile file) {
-    if (file.getName().endsWith(".java") && SyncStatusHelper.isUnsynced(project, file)) {
+    if (file.getName().endsWith(".java")
+        && SyncStatusHelper.getInstance(project).isUnsynced(file)) {
       return UNSYNCED_COLOR;
     }
     return null;
diff --git a/java/src/com/google/idea/blaze/java/syncstatus/BlazeJavaSyncStatusEditorTabTitleProvider.java b/java/src/com/google/idea/blaze/java/syncstatus/BlazeJavaSyncStatusEditorTabTitleProvider.java
index fce5c89..aa96de8 100644
--- a/java/src/com/google/idea/blaze/java/syncstatus/BlazeJavaSyncStatusEditorTabTitleProvider.java
+++ b/java/src/com/google/idea/blaze/java/syncstatus/BlazeJavaSyncStatusEditorTabTitleProvider.java
@@ -25,7 +25,7 @@
   @Nullable
   @Override
   public String getEditorTabTitle(Project project, VirtualFile file) {
-    if (file.getName().endsWith("java") && SyncStatusHelper.isUnsynced(project, file)) {
+    if (file.getName().endsWith("java") && SyncStatusHelper.getInstance(project).isUnsynced(file)) {
       return file.getPresentableName() + " (unsynced)";
     }
     return null;
diff --git a/java/src/com/google/idea/blaze/java/syncstatus/SyncStatusHelper.java b/java/src/com/google/idea/blaze/java/syncstatus/SyncStatusHelper.java
index 2ea5884..74fc43f 100644
--- a/java/src/com/google/idea/blaze/java/syncstatus/SyncStatusHelper.java
+++ b/java/src/com/google/idea/blaze/java/syncstatus/SyncStatusHelper.java
@@ -15,29 +15,57 @@
  */
 package com.google.idea.blaze.java.syncstatus;
 
+import com.google.common.collect.ImmutableSet;
 import com.google.idea.blaze.base.model.BlazeProjectData;
-import com.google.idea.blaze.base.sync.data.BlazeProjectDataManager;
+import com.google.idea.blaze.base.projectview.ProjectViewSet;
+import com.google.idea.blaze.base.scope.BlazeContext;
+import com.google.idea.blaze.base.settings.BlazeImportSettings;
+import com.google.idea.blaze.base.sync.SyncListener;
+import com.google.idea.blaze.base.sync.workspace.ArtifactLocationDecoder;
 import com.google.idea.blaze.java.sync.model.BlazeJavaSyncData;
+import com.intellij.openapi.components.ServiceManager;
 import com.intellij.openapi.project.Project;
 import com.intellij.openapi.vfs.VirtualFile;
 import java.io.File;
+import java.util.Set;
 
 class SyncStatusHelper {
-  static boolean isUnsynced(Project project, VirtualFile virtualFile) {
-    BlazeProjectData blazeProjectData =
-        BlazeProjectDataManager.getInstance(project).getBlazeProjectData();
-    if (blazeProjectData == null) {
-      return false;
-    }
-    BlazeJavaSyncData syncData = blazeProjectData.syncState.get(BlazeJavaSyncData.class);
-    if (syncData == null) {
-      return false;
-    }
+  static SyncStatusHelper getInstance(Project project) {
+    return ServiceManager.getService(project, SyncStatusHelper.class);
+  }
+
+  private Set<File> syncedJavaFiles = ImmutableSet.of();
+
+  boolean isUnsynced(VirtualFile virtualFile) {
     if (!virtualFile.isInLocalFileSystem()) {
       return false;
     }
-
     File file = new File(virtualFile.getPath());
-    return !syncData.importResult.javaSourceFiles.contains(file);
+    return !syncedJavaFiles.contains(file);
+  }
+
+  void refresh(BlazeProjectData blazeProjectData) {
+    BlazeJavaSyncData syncData = blazeProjectData.syncState.get(BlazeJavaSyncData.class);
+    if (syncData == null) {
+      return;
+    }
+    ArtifactLocationDecoder artifactLocationDecoder = blazeProjectData.artifactLocationDecoder;
+    syncedJavaFiles =
+        ImmutableSet.<File>builder()
+            .addAll(artifactLocationDecoder.decodeAll(syncData.importResult.javaSourceFiles))
+            .build();
+  }
+
+  static class UpdateSyncStatusMap extends SyncListener.Adapter {
+    @Override
+    public void onSyncComplete(
+        Project project,
+        BlazeContext context,
+        BlazeImportSettings importSettings,
+        ProjectViewSet projectViewSet,
+        BlazeProjectData blazeProjectData,
+        SyncResult syncResult) {
+      getInstance(project).refresh(blazeProjectData);
+    }
   }
 }
diff --git a/java/tests/integrationtests/com/google/idea/blaze/java/lang/build/JavaClassRenameTest.java b/java/tests/integrationtests/com/google/idea/blaze/java/lang/build/JavaClassRenameTest.java
index 7a1a64d..04c83db 100644
--- a/java/tests/integrationtests/com/google/idea/blaze/java/lang/build/JavaClassRenameTest.java
+++ b/java/tests/integrationtests/com/google/idea/blaze/java/lang/build/JavaClassRenameTest.java
@@ -19,10 +19,15 @@
 import com.google.idea.blaze.base.lang.buildfile.psi.BuildFile;
 import com.intellij.psi.PsiJavaFile;
 import com.intellij.refactoring.rename.RenameProcessor;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
 
 /** Tests that BUILD file references are correctly updated when performing rename refactors. */
+@RunWith(JUnit4.class)
 public class JavaClassRenameTest extends BuildFileIntegrationTestCase {
 
+  @Test
   public void testRenameJavaClass() {
     PsiJavaFile javaFile =
         (PsiJavaFile)
diff --git a/java/tests/integrationtests/com/google/idea/blaze/java/lang/build/SafeDeleteTest.java b/java/tests/integrationtests/com/google/idea/blaze/java/lang/build/SafeDeleteTest.java
index 535932a..bdee4a1 100644
--- a/java/tests/integrationtests/com/google/idea/blaze/java/lang/build/SafeDeleteTest.java
+++ b/java/tests/integrationtests/com/google/idea/blaze/java/lang/build/SafeDeleteTest.java
@@ -16,58 +16,61 @@
 package com.google.idea.blaze.java.lang.build;
 
 import com.google.idea.blaze.base.lang.buildfile.BuildFileIntegrationTestCase;
-import com.google.idea.blaze.base.lang.buildfile.psi.BuildFile;
 import com.google.idea.blaze.base.lang.buildfile.psi.util.PsiUtils;
 import com.intellij.psi.PsiClass;
 import com.intellij.psi.PsiElement;
 import com.intellij.psi.PsiFile;
 import com.intellij.refactoring.BaseRefactoringProcessor;
 import com.intellij.refactoring.safeDelete.SafeDeleteHandler;
+import org.junit.Assert;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
 
 /** Tests for the safe delete action which aren't covered by existing tests. */
+@RunWith(JUnit4.class)
 public class SafeDeleteTest extends BuildFileIntegrationTestCase {
 
+  @Test
   public void testIndirectGlobReferencesNotIncluded() {
     PsiFile javaFile =
         createPsiFile("com/google/Test.java", "package com.google;", "public class Test {}");
 
     PsiClass javaClass = PsiUtils.findFirstChildOfClassRecursive(javaFile, PsiClass.class);
 
-    BuildFile buildFile =
-        createBuildFile(
-            "com/google/BUILD",
-            "java_library(",
-            "    name = 'lib'",
-            "    srcs = glob(['*.java'])",
-            ")");
+    createBuildFile(
+        "com/google/BUILD",
+        "java_library(",
+        "    name = 'lib'",
+        "    srcs = glob(['*.java'])",
+        ")");
 
     try {
       SafeDeleteHandler.invoke(getProject(), new PsiElement[] {javaClass}, true);
     } catch (BaseRefactoringProcessor.ConflictsInTestsException e) {
-      fail("Glob reference was incorrectly included");
-      return;
+      Assert.fail("Glob reference was incorrectly included");
     }
   }
 
+  @Test
   public void testDirectGlobReferencesIncluded() {
     PsiFile javaFile =
         createPsiFile("com/google/Test.java", "package com.google;", "public class Test {}");
 
     PsiClass javaClass = PsiUtils.findFirstChildOfClassRecursive(javaFile, PsiClass.class);
 
-    BuildFile buildFile =
-        createBuildFile(
-            "com/google/BUILD",
-            "java_library(",
-            "    name = 'lib'",
-            "    srcs = glob(['Test.java'])",
-            ")");
+    createBuildFile(
+        "com/google/BUILD",
+        "java_library(",
+        "    name = 'lib'",
+        "    srcs = glob(['Test.java'])",
+        ")");
 
     try {
       SafeDeleteHandler.invoke(getProject(), new PsiElement[] {javaClass}, true);
     } catch (BaseRefactoringProcessor.ConflictsInTestsException expected) {
       return;
     }
-    fail("Expected an unsafe usage to be found");
+    Assert.fail("Expected an unsafe usage to be found");
   }
 }
diff --git a/java/tests/integrationtests/com/google/idea/blaze/java/sync/JavaSyncTest.java b/java/tests/integrationtests/com/google/idea/blaze/java/sync/JavaSyncTest.java
index 76dc9e1..717e398 100644
--- a/java/tests/integrationtests/com/google/idea/blaze/java/sync/JavaSyncTest.java
+++ b/java/tests/integrationtests/com/google/idea/blaze/java/sync/JavaSyncTest.java
@@ -19,9 +19,9 @@
 
 import com.google.idea.blaze.base.ideinfo.JavaRuleIdeInfo;
 import com.google.idea.blaze.base.ideinfo.RuleIdeInfo;
+import com.google.idea.blaze.base.ideinfo.RuleMap;
 import com.google.idea.blaze.base.ideinfo.RuleMapBuilder;
 import com.google.idea.blaze.base.model.BlazeProjectData;
-import com.google.idea.blaze.base.model.RuleMap;
 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;
@@ -31,10 +31,15 @@
 import com.google.idea.blaze.java.sync.model.BlazeJavaSyncData;
 import com.google.idea.blaze.java.sync.model.BlazeSourceDirectory;
 import java.util.List;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
 
 /** Java-specific sync integration tests. */
+@RunWith(JUnit4.class)
 public class JavaSyncTest extends BlazeSyncIntegrationTestCase {
 
+  @Test
   public void testJavaClassesPresentInClassPath() throws Exception {
     setProjectView(
         "directories:",
@@ -97,6 +102,7 @@
         .isEqualTo(tempDirectory.getPath() + "/java/com/google");
   }
 
+  @Test
   public void testSimpleSync() throws Exception {
     setProjectView(
         "directories:",
diff --git a/java/tests/unittests/com/google/idea/blaze/java/run/BlazeJavaRunProfileStateTest.java b/java/tests/unittests/com/google/idea/blaze/java/run/BlazeJavaRunProfileStateTest.java
index b3dd584..51c2e6d 100644
--- a/java/tests/unittests/com/google/idea/blaze/java/run/BlazeJavaRunProfileStateTest.java
+++ b/java/tests/unittests/com/google/idea/blaze/java/run/BlazeJavaRunProfileStateTest.java
@@ -29,10 +29,10 @@
 import com.google.idea.blaze.base.projectview.ProjectViewSet;
 import com.google.idea.blaze.base.run.BlazeCommandRunConfiguration;
 import com.google.idea.blaze.base.run.BlazeCommandRunConfigurationType;
-import com.google.idea.blaze.base.run.confighandler.BlazeCommandGenericRunConfigurationHandler;
 import com.google.idea.blaze.base.run.confighandler.BlazeCommandGenericRunConfigurationHandlerProvider;
 import com.google.idea.blaze.base.run.confighandler.BlazeCommandRunConfigurationHandlerProvider;
 import com.google.idea.blaze.base.run.rulefinder.RuleFinder;
+import com.google.idea.blaze.base.run.state.BlazeCommandRunConfigurationCommonState;
 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;
@@ -63,9 +63,6 @@
         BlazeImportSettingsManager.class, new BlazeImportSettingsManager(project));
     BlazeImportSettingsManager.getInstance(getProject()).setImportSettings(DUMMY_IMPORT_SETTINGS);
 
-    configuration =
-        new BlazeCommandRunConfigurationType().getFactory().createTemplateConfiguration(project);
-
     ExperimentService experimentService = new MockExperimentService();
     applicationServices.register(ExperimentService.class, experimentService);
     applicationServices.register(RuleFinder.class, new MockRuleFinder());
@@ -76,15 +73,18 @@
             BlazeCommandRunConfigurationHandlerProvider.EP_NAME,
             BlazeCommandRunConfigurationHandlerProvider.class);
     handlerProviderEp.registerExtension(new BlazeCommandGenericRunConfigurationHandlerProvider());
+
+    configuration =
+        new BlazeCommandRunConfigurationType().getFactory().createTemplateConfiguration(project);
   }
 
   @Test
   public void flagsShouldBeAppendedIfPresent() {
     configuration.setTarget(new Label("//label:rule"));
-    BlazeCommandGenericRunConfigurationHandler handler =
-        (BlazeCommandGenericRunConfigurationHandler) configuration.getHandler();
-    handler.setCommand(BlazeCommandName.fromString("command"));
-    handler.setBlazeFlags(ImmutableList.of("--flag1", "--flag2"));
+    BlazeCommandRunConfigurationCommonState handlerState =
+        (BlazeCommandRunConfigurationCommonState) configuration.getHandler().getState();
+    handlerState.setCommand(BlazeCommandName.fromString("command"));
+    handlerState.setBlazeFlags(ImmutableList.of("--flag1", "--flag2"));
     assertThat(
             BlazeJavaRunProfileState.getBlazeCommand(
                     project, configuration, ProjectViewSet.builder().build(), false /* debug */)
@@ -103,9 +103,9 @@
   @Test
   public void debugFlagShouldBeIncludedForJavaTest() {
     configuration.setTarget(new Label("//label:rule"));
-    BlazeCommandGenericRunConfigurationHandler handler =
-        (BlazeCommandGenericRunConfigurationHandler) configuration.getHandler();
-    handler.setCommand(BlazeCommandName.fromString("command"));
+    BlazeCommandRunConfigurationCommonState handlerState =
+        (BlazeCommandRunConfigurationCommonState) configuration.getHandler().getState();
+    handlerState.setCommand(BlazeCommandName.fromString("command"));
     assertThat(
             BlazeJavaRunProfileState.getBlazeCommand(
                     project, configuration, ProjectViewSet.builder().build(), true /* debug */)
@@ -123,9 +123,9 @@
   @Test
   public void debugFlagShouldBeIncludedForJavaBinary() {
     configuration.setTarget(new Label("//label:java_binary_rule"));
-    BlazeCommandGenericRunConfigurationHandler handler =
-        (BlazeCommandGenericRunConfigurationHandler) configuration.getHandler();
-    handler.setCommand(BlazeCommandName.fromString("command"));
+    BlazeCommandRunConfigurationCommonState handlerState =
+        (BlazeCommandRunConfigurationCommonState) configuration.getHandler().getState();
+    handlerState.setCommand(BlazeCommandName.fromString("command"));
     assertThat(
             BlazeJavaRunProfileState.getBlazeCommand(
                     project, configuration, ProjectViewSet.builder().build(), true /* debug */)
diff --git a/java/tests/unittests/com/google/idea/blaze/java/sync/importer/BlazeJavaWorkspaceImporterTest.java b/java/tests/unittests/com/google/idea/blaze/java/sync/importer/BlazeJavaWorkspaceImporterTest.java
index 9b5aa31..c5461f1 100644
--- a/java/tests/unittests/com/google/idea/blaze/java/sync/importer/BlazeJavaWorkspaceImporterTest.java
+++ b/java/tests/unittests/com/google/idea/blaze/java/sync/importer/BlazeJavaWorkspaceImporterTest.java
@@ -34,9 +34,10 @@
 import com.google.idea.blaze.base.ideinfo.LibraryArtifact;
 import com.google.idea.blaze.base.ideinfo.ProtoLibraryLegacyInfo;
 import com.google.idea.blaze.base.ideinfo.RuleIdeInfo;
+import com.google.idea.blaze.base.ideinfo.RuleKey;
+import com.google.idea.blaze.base.ideinfo.RuleMap;
 import com.google.idea.blaze.base.ideinfo.RuleMapBuilder;
 import com.google.idea.blaze.base.ideinfo.Tags;
-import com.google.idea.blaze.base.model.primitives.ExecutionRootPath;
 import com.google.idea.blaze.base.model.primitives.Label;
 import com.google.idea.blaze.base.model.primitives.LanguageClass;
 import com.google.idea.blaze.base.model.primitives.WorkspacePath;
@@ -61,7 +62,6 @@
 import com.google.idea.blaze.base.settings.BlazeImportSettingsManager;
 import com.google.idea.blaze.base.sync.projectview.WorkspaceLanguageSettings;
 import com.google.idea.blaze.base.sync.workspace.ArtifactLocationDecoder;
-import com.google.idea.blaze.base.sync.workspace.BlazeRoots;
 import com.google.idea.blaze.base.sync.workspace.WorkingSet;
 import com.google.idea.blaze.java.sync.BlazeJavaSyncAugmenter;
 import com.google.idea.blaze.java.sync.jdeps.JdepsMap;
@@ -98,33 +98,26 @@
   private static final String FAKE_GEN_ROOT_EXECUTION_PATH_FRAGMENT =
       "blaze-out/gcc-4.X.Y-crosstool-v17-hybrid-grtev3-k8-fastbuild/bin";
 
-  private static final String FAKE_GEN_ROOT =
-      "/path/to/8093958afcfde6c33d08b621dfaa4e09/root/" + FAKE_GEN_ROOT_EXECUTION_PATH_FRAGMENT;
 
   private static final ArtifactLocationDecoder FAKE_ARTIFACT_DECODER =
-      new ArtifactLocationDecoder(
-          new BlazeRoots(
-              new File("/"),
-              ImmutableList.of(),
-              new ExecutionRootPath("out/crosstool/bin"),
-              new ExecutionRootPath("out/crosstool/gen")),
-          null);
+      (ArtifactLocationDecoder)
+          artifactLocation -> new File("/", artifactLocation.getRelativePath());
 
   private static final BlazeImportSettings DUMMY_IMPORT_SETTINGS =
       new BlazeImportSettings("", "", "", "", "", BuildSystem.Blaze);
   private ExtensionPointImpl<BlazeJavaSyncAugmenter> augmenters;
 
   private static class JdepsMock implements JdepsMap {
-    Map<Label, List<String>> jdeps = Maps.newHashMap();
+    Map<RuleKey, List<String>> jdeps = Maps.newHashMap();
 
     @Nullable
     @Override
-    public List<String> getDependenciesForRule(@NotNull Label label) {
-      return jdeps.get(label);
+    public List<String> getDependenciesForRule(RuleKey ruleKey) {
+      return jdeps.get(ruleKey);
     }
 
-    JdepsMock put(Label label, List<String> values) {
-      jdeps.put(label, values);
+    JdepsMock put(RuleKey ruleKey, List<String> values) {
+      jdeps.put(ruleKey, values);
       return this;
     }
   }
@@ -156,7 +149,9 @@
           @Nullable
           @Override
           public String getDeclaredPackageOfJavaFile(
-              @NotNull BlazeContext context, @NotNull SourceArtifact sourceArtifact) {
+              BlazeContext context,
+              ArtifactLocationDecoder artifactLocationDecoder,
+              SourceArtifact sourceArtifact) {
             return null;
           }
         });
@@ -175,14 +170,17 @@
 
     ProjectViewSet projectViewSet = ProjectViewSet.builder().add(projectView).build();
 
+    RuleMap ruleMap = ruleMapBuilder.build();
+    JavaSourceFilter sourceFilter =
+        new JavaSourceFilter(project, workspaceRoot, projectViewSet, ruleMap);
     BlazeJavaWorkspaceImporter blazeWorkspaceImporter =
         new BlazeJavaWorkspaceImporter(
             project,
-            context,
             workspaceRoot,
             projectViewSet,
             workspaceLanguageSettings,
-            ruleMapBuilder.build(),
+            ruleMap,
+            sourceFilter,
             jdepsMap,
             workingSet,
             FAKE_ARTIFACT_DECODER);
@@ -235,9 +233,9 @@
     errorCollector.assertNoIssues();
 
     assertEquals(1, result.buildOutputJars.size());
-    File compilerOutputLib = result.buildOutputJars.iterator().next();
+    ArtifactLocation compilerOutputLib = result.buildOutputJars.iterator().next();
     assertNotNull(compilerOutputLib);
-    assertTrue(compilerOutputLib.getPath().endsWith("example_debug.jar"));
+    assertTrue(compilerOutputLib.relativePath.endsWith("example_debug.jar"));
 
     assertThat(result.contentEntries)
         .containsExactly(
@@ -250,8 +248,8 @@
 
     assertThat(result.javaSourceFiles)
         .containsExactly(
-            source("java/apps/example/MainActivity.java").getFile(),
-            source("java/apps/example/subdir/SubdirHelper.java").getFile());
+            source("java/apps/example/MainActivity.java"),
+            source("java/apps/example/subdir/SubdirHelper.java"));
   }
 
   @Test
@@ -374,7 +372,7 @@
                         .build())
                 .build());
     assertThat(result.javaSourceFiles)
-        .containsExactly(source("java/apps/example/MainActivity.java").getFile());
+        .containsExactly(source("java/apps/example/MainActivity.java"));
   }
 
   /** Import a project and its tests */
@@ -992,7 +990,7 @@
             .build();
     RuleMapBuilder ruleMapBuilder = ruleMapForJdepsSuite();
     jdepsMap.put(
-        new Label("//java/apps/example:example_debug"),
+        RuleKey.forPlainTarget(new Label("//java/apps/example:example_debug")),
         Lists.newArrayList(jdepsPath("thirdparty/a.jar"), jdepsPath("thirdparty/c.jar")));
 
     BlazeJavaImportResult result = importWorkspace(workspaceRoot, ruleMapBuilder, projectView);
@@ -1220,7 +1218,7 @@
 
     // First test - make sure that jdeps is working
     jdepsMap.put(
-        new Label("//java/example:liba"),
+        RuleKey.forPlainTarget(new Label("//java/example:liba")),
         Lists.newArrayList(jdepsPath("thirdparty/proto/a/liba-ijar.jar")));
     BlazeJavaImportResult result = importWorkspace(workspaceRoot, ruleMapBuilder, projectView);
     errorCollector.assertNoIssues();
@@ -1338,7 +1336,7 @@
               jars.add(
                   new BlazeJarLibrary(
                       LibraryArtifact.builder().setInterfaceJar(gen("source.jar")).build(),
-                      rule.label));
+                      rule.key));
             }
           }
         });
@@ -1382,19 +1380,14 @@
   /* Utility methods */
 
   private static String libraryFileName(BlazeJarLibrary library) {
-    return library.libraryArtifact.jarForIntellijLibrary().getFile().getName();
+    return new File(library.libraryArtifact.jarForIntellijLibrary().relativePath).getName();
   }
 
   @Nullable
   private static BlazeJarLibrary findLibrary(
       Map<LibraryKey, BlazeJarLibrary> libraries, String libraryName) {
     for (BlazeJarLibrary library : libraries.values()) {
-      if (library
-          .libraryArtifact
-          .jarForIntellijLibrary()
-          .getFile()
-          .getPath()
-          .endsWith(libraryName)) {
+      if (library.libraryArtifact.jarForIntellijLibrary().relativePath.endsWith(libraryName)) {
         return library;
       }
     }
@@ -1403,7 +1396,6 @@
 
   private ArtifactLocation source(String relativePath) {
     return ArtifactLocation.builder()
-        .setRootPath(FAKE_WORKSPACE_ROOT)
         .setRelativePath(relativePath)
         .setIsSource(true)
         .build();
@@ -1411,7 +1403,6 @@
 
   private static ArtifactLocation gen(String relativePath) {
     return ArtifactLocation.builder()
-        .setRootPath(FAKE_GEN_ROOT)
         .setRootExecutionPathFragment(FAKE_GEN_ROOT_EXECUTION_PATH_FRAGMENT)
         .setRelativePath(relativePath)
         .setIsSource(false)
diff --git a/java/tests/unittests/com/google/idea/blaze/java/sync/source/SourceDirectoryCalculatorTest.java b/java/tests/unittests/com/google/idea/blaze/java/sync/source/SourceDirectoryCalculatorTest.java
index dec8f90..684cd7f 100644
--- a/java/tests/unittests/com/google/idea/blaze/java/sync/source/SourceDirectoryCalculatorTest.java
+++ b/java/tests/unittests/com/google/idea/blaze/java/sync/source/SourceDirectoryCalculatorTest.java
@@ -20,12 +20,12 @@
 
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableMap;
-import com.google.common.collect.Lists;
 import com.google.common.util.concurrent.MoreExecutors;
 import com.google.idea.blaze.base.BlazeTestCase;
 import com.google.idea.blaze.base.async.executor.BlazeExecutor;
 import com.google.idea.blaze.base.async.executor.MockBlazeExecutor;
 import com.google.idea.blaze.base.ideinfo.ArtifactLocation;
+import com.google.idea.blaze.base.ideinfo.RuleKey;
 import com.google.idea.blaze.base.io.FileAttributeProvider;
 import com.google.idea.blaze.base.io.InputStreamProvider;
 import com.google.idea.blaze.base.model.primitives.ExecutionRootPath;
@@ -40,6 +40,7 @@
 import com.google.idea.blaze.base.scope.output.IssueOutput;
 import com.google.idea.blaze.base.sync.projectview.SourceTestConfig;
 import com.google.idea.blaze.base.sync.workspace.ArtifactLocationDecoder;
+import com.google.idea.blaze.base.sync.workspace.ArtifactLocationDecoderImpl;
 import com.google.idea.blaze.base.sync.workspace.BlazeRoots;
 import com.google.idea.blaze.base.sync.workspace.WorkspacePathResolverImpl;
 import com.google.idea.blaze.java.sync.model.BlazeContentEntry;
@@ -66,7 +67,7 @@
 @RunWith(JUnit4.class)
 public class SourceDirectoryCalculatorTest extends BlazeTestCase {
 
-  private static final ImmutableMap<Label, ArtifactLocation> NO_MANIFESTS = ImmutableMap.of();
+  private static final ImmutableMap<RuleKey, ArtifactLocation> NO_MANIFESTS = ImmutableMap.of();
   private static final Label LABEL = new Label("//fake:label");
 
   private MockInputStreamProvider mockInputStreamProvider;
@@ -78,13 +79,8 @@
 
   private WorkspaceRoot workspaceRoot = new WorkspaceRoot(new File("/root"));
   private ArtifactLocationDecoder decoder =
-      new ArtifactLocationDecoder(
-          new BlazeRoots(
-              new File("/"),
-              Lists.newArrayList(new File("/usr/local/code")),
-              new ExecutionRootPath("out/crosstool/bin"),
-              new ExecutionRootPath("out/crosstool/gen")),
-          null);
+      (ArtifactLocationDecoder)
+          artifactLocation -> new File("/root", artifactLocation.getRelativePath());
 
   static final class TestSourceImportConfig extends SourceTestConfig {
     final boolean isTest;
@@ -153,11 +149,10 @@
         "/root/java/com/google/Bla.java", "package com.google;\n public class Bla {}");
     List<SourceArtifact> sourceArtifacts =
         ImmutableList.of(
-            SourceArtifact.builder(LABEL)
+            SourceArtifact.builder(RuleKey.forPlainTarget(LABEL))
                 .setArtifactLocation(
                     ArtifactLocation.builder()
                         .setRelativePath("java/com/google/Bla.java")
-                        .setRootPath("/root")
                         .setIsSource(true))
                 .build());
     ImmutableList<BlazeContentEntry> result =
@@ -187,11 +182,10 @@
         "/root/java/com/google/Bla.java", "package com.google;\n public class Bla {}");
     List<SourceArtifact> sourceArtifacts =
         ImmutableList.of(
-            SourceArtifact.builder(LABEL)
+            SourceArtifact.builder(RuleKey.forPlainTarget(LABEL))
                 .setArtifactLocation(
                     ArtifactLocation.builder()
                         .setRelativePath("java/com/google/Bla.java")
-                        .setRootPath("/root")
                         .setIsSource(true))
                 .build());
     ImmutableList<BlazeContentEntry> result =
@@ -225,18 +219,16 @@
             "package com.google.subpackage;\n public class Bla {}");
     List<SourceArtifact> sourceArtifacts =
         ImmutableList.of(
-            SourceArtifact.builder(LABEL)
+            SourceArtifact.builder(RuleKey.forPlainTarget(LABEL))
                 .setArtifactLocation(
                     ArtifactLocation.builder()
                         .setRelativePath("java/com/google/Bla.java")
-                        .setRootPath("/root")
                         .setIsSource(true))
                 .build(),
-            SourceArtifact.builder(LABEL)
+            SourceArtifact.builder(RuleKey.forPlainTarget(LABEL))
                 .setArtifactLocation(
                     ArtifactLocation.builder()
                         .setRelativePath("java/com/google/subpackage/Bla.java")
-                        .setRootPath("/root")
                         .setIsSource(true))
                 .build());
     ImmutableList<BlazeContentEntry> result =
@@ -274,25 +266,22 @@
             "package com.google.idea.blaze.plugin;\n public class Plugin {}");
     List<SourceArtifact> sourceArtifacts =
         ImmutableList.of(
-            SourceArtifact.builder(LABEL)
+            SourceArtifact.builder(RuleKey.forPlainTarget(LABEL))
                 .setArtifactLocation(
                     ArtifactLocation.builder()
                         .setRelativePath("java/com/google/idea/blaze/plugin/run/Run.java")
-                        .setRootPath("/root")
                         .setIsSource(true))
                 .build(),
-            SourceArtifact.builder(LABEL)
+            SourceArtifact.builder(RuleKey.forPlainTarget(LABEL))
                 .setArtifactLocation(
                     ArtifactLocation.builder()
                         .setRelativePath("java/com/google/idea/blaze/plugin/sync/Sync.java")
-                        .setRootPath("/root")
                         .setIsSource(true))
                 .build(),
-            SourceArtifact.builder(LABEL)
+            SourceArtifact.builder(RuleKey.forPlainTarget(LABEL))
                 .setArtifactLocation(
                     ArtifactLocation.builder()
                         .setRelativePath("java/com/google/idea/blaze/plugin/Plugin.java")
-                        .setRootPath("/root")
                         .setIsSource(true))
                 .build());
     ImmutableList<BlazeContentEntry> result =
@@ -328,25 +317,22 @@
             "package com.google.idea.blaze.incorrect;\n public class Incorrect {}");
     List<SourceArtifact> sourceArtifacts =
         ImmutableList.of(
-            SourceArtifact.builder(LABEL)
+            SourceArtifact.builder(RuleKey.forPlainTarget(LABEL))
                 .setArtifactLocation(
                     ArtifactLocation.builder()
                         .setRelativePath("java/com/google/idea/blaze/plugin/run/Run.java")
-                        .setRootPath("/root")
                         .setIsSource(true))
                 .build(),
-            SourceArtifact.builder(LABEL)
+            SourceArtifact.builder(RuleKey.forPlainTarget(LABEL))
                 .setArtifactLocation(
                     ArtifactLocation.builder()
                         .setRelativePath("java/com/google/idea/blaze/plugin/sync/Sync.java")
-                        .setRootPath("/root")
                         .setIsSource(true))
                 .build(),
-            SourceArtifact.builder(LABEL)
+            SourceArtifact.builder(RuleKey.forPlainTarget(LABEL))
                 .setArtifactLocation(
                     ArtifactLocation.builder()
                         .setRelativePath("java/com/google/idea/blaze/Incorrect.java")
-                        .setRootPath("/root")
                         .setIsSource(true))
                 .build());
     ImmutableList<BlazeContentEntry> result =
@@ -386,18 +372,16 @@
             "package com.google.different;\n public class Bla {}");
     List<SourceArtifact> sourceArtifacts =
         ImmutableList.of(
-            SourceArtifact.builder(LABEL)
+            SourceArtifact.builder(RuleKey.forPlainTarget(LABEL))
                 .setArtifactLocation(
                     ArtifactLocation.builder()
                         .setRelativePath("java/com/google/Bla.java")
-                        .setRootPath("/root")
                         .setIsSource(true))
                 .build(),
-            SourceArtifact.builder(LABEL)
+            SourceArtifact.builder(RuleKey.forPlainTarget(LABEL))
                 .setArtifactLocation(
                     ArtifactLocation.builder()
                         .setRelativePath("java/com/google/subpackage/Bla.java")
-                        .setRootPath("/root")
                         .setIsSource(true))
                 .build());
     ImmutableList<BlazeContentEntry> result =
@@ -434,18 +418,16 @@
             "package com.google.subpackage;\n public class Bla {}");
     List<SourceArtifact> sourceArtifacts =
         ImmutableList.of(
-            SourceArtifact.builder(LABEL)
+            SourceArtifact.builder(RuleKey.forPlainTarget(LABEL))
                 .setArtifactLocation(
                     ArtifactLocation.builder()
                         .setRelativePath("java/com/google/Bla.java")
-                        .setRootPath("/root")
                         .setIsSource(true))
                 .build(),
-            SourceArtifact.builder(LABEL)
+            SourceArtifact.builder(RuleKey.forPlainTarget(LABEL))
                 .setArtifactLocation(
                     ArtifactLocation.builder()
                         .setRelativePath("java/com/google/subpackage/Bla.java")
-                        .setRootPath("/root")
                         .setIsSource(true))
                 .build());
     ImmutableList<BlazeContentEntry> result =
@@ -482,18 +464,16 @@
             "package com.google.different;\n public class Bla {}");
     List<SourceArtifact> sourceArtifacts =
         ImmutableList.of(
-            SourceArtifact.builder(LABEL)
+            SourceArtifact.builder(RuleKey.forPlainTarget(LABEL))
                 .setArtifactLocation(
                     ArtifactLocation.builder()
                         .setRelativePath("java/com/google/subpackage/Bla.java")
-                        .setRootPath("/root")
                         .setIsSource(true))
                 .build(),
-            SourceArtifact.builder(LABEL)
+            SourceArtifact.builder(RuleKey.forPlainTarget(LABEL))
                 .setArtifactLocation(
                     ArtifactLocation.builder()
                         .setRelativePath("java/com/google/Bla.java")
-                        .setRootPath("/root")
                         .setIsSource(true))
                 .build());
     ImmutableList<BlazeContentEntry> result =
@@ -527,11 +507,10 @@
         "/root/java/com/google/Bla.java", "package com.google;\n public class Bla {}");
     List<SourceArtifact> sourceArtifacts =
         ImmutableList.of(
-            SourceArtifact.builder(LABEL)
+            SourceArtifact.builder(RuleKey.forPlainTarget(LABEL))
                 .setArtifactLocation(
                     ArtifactLocation.builder()
                         .setRelativePath("java/com/google/Bla.java")
-                        .setRootPath("/root")
                         .setIsSource(true))
                 .build());
     ImmutableList<BlazeContentEntry> result =
@@ -561,11 +540,10 @@
         "/root/java/com/google/Bla.java", "package com.facebook;\n public class Bla {}");
     List<SourceArtifact> sourceArtifacts =
         ImmutableList.of(
-            SourceArtifact.builder(LABEL)
+            SourceArtifact.builder(RuleKey.forPlainTarget(LABEL))
                 .setArtifactLocation(
                     ArtifactLocation.builder()
                         .setRelativePath("java/com/google/Bla.java")
-                        .setRootPath("/root")
                         .setIsSource(true))
                 .build());
     ImmutableList<BlazeContentEntry> result =
@@ -595,11 +573,10 @@
         "/root/java/com/org/foo/Bla.java", "package com.facebook;\n public class Bla {}");
     List<SourceArtifact> sourceArtifacts =
         ImmutableList.of(
-            SourceArtifact.builder(LABEL)
+            SourceArtifact.builder(RuleKey.forPlainTarget(LABEL))
                 .setArtifactLocation(
                     ArtifactLocation.builder()
                         .setRelativePath("java/com/org/foo/Bla.java")
-                        .setRootPath("/root")
                         .setIsSource(true))
                 .build());
     ImmutableList<BlazeContentEntry> result =
@@ -633,23 +610,21 @@
         "/root/java/com/facebook/Bla.java", "package com.facebook;\n public class Bla {}");
     List<SourceArtifact> sourceArtifacts =
         ImmutableList.of(
-            SourceArtifact.builder(LABEL)
+            SourceArtifact.builder(RuleKey.forPlainTarget(LABEL))
                 .setArtifactLocation(
                     ArtifactLocation.builder()
                         .setRelativePath("java/com/facebook/Bla.java")
-                        .setRootPath("/root")
                         .setIsSource(true))
                 .build());
-    ImmutableList<BlazeContentEntry> result =
-        sourceDirectoryCalculator.calculateContentEntries(
-            project,
-            context,
-            workspaceRoot,
-            new TestSourceImportConfig(false),
-            decoder,
-            ImmutableList.of(new WorkspacePath("java/com/google")),
-            sourceArtifacts,
-            NO_MANIFESTS);
+    sourceDirectoryCalculator.calculateContentEntries(
+        project,
+        context,
+        workspaceRoot,
+        new TestSourceImportConfig(false),
+        decoder,
+        ImmutableList.of(new WorkspacePath("java/com/google")),
+        sourceArtifacts,
+        NO_MANIFESTS);
 
     issues.assertIssueContaining("Did not add");
   }
@@ -661,23 +636,21 @@
         "/root/java/com/facebook/Bla.java", "package com.facebook;\n public class Bla {}");
     List<SourceArtifact> sourceArtifacts =
         ImmutableList.of(
-            SourceArtifact.builder(LABEL)
+            SourceArtifact.builder(RuleKey.forPlainTarget(LABEL))
                 .setArtifactLocation(
                     ArtifactLocation.builder()
                         .setRelativePath("java/com/facebook/Bla.java")
-                        .setRootPath("/root")
                         .setIsSource(false))
                 .build());
-    ImmutableList<BlazeContentEntry> result =
-        sourceDirectoryCalculator.calculateContentEntries(
-            project,
-            context,
-            workspaceRoot,
-            new TestSourceImportConfig(false),
-            decoder,
-            ImmutableList.of(new WorkspacePath("java/com/google/my")),
-            sourceArtifacts,
-            NO_MANIFESTS);
+    sourceDirectoryCalculator.calculateContentEntries(
+        project,
+        context,
+        workspaceRoot,
+        new TestSourceImportConfig(false),
+        decoder,
+        ImmutableList.of(new WorkspacePath("java/com/google/my")),
+        sourceArtifacts,
+        NO_MANIFESTS);
     issues.assertNoIssues();
   }
 
@@ -686,23 +659,21 @@
     mockInputStreamProvider.addFile("/root/java/com/google/Bla.java", "public class Bla {}");
     List<SourceArtifact> sourceArtifacts =
         ImmutableList.of(
-            SourceArtifact.builder(LABEL)
+            SourceArtifact.builder(RuleKey.forPlainTarget(LABEL))
                 .setArtifactLocation(
                     ArtifactLocation.builder()
                         .setRelativePath("java/com/google/Bla.java")
-                        .setRootPath("/root")
                         .setIsSource(true))
                 .build());
-    ImmutableList<BlazeContentEntry> result =
-        sourceDirectoryCalculator.calculateContentEntries(
-            project,
-            context,
-            workspaceRoot,
-            new TestSourceImportConfig(false),
-            decoder,
-            ImmutableList.of(new WorkspacePath("java/com/google")),
-            sourceArtifacts,
-            NO_MANIFESTS);
+    sourceDirectoryCalculator.calculateContentEntries(
+        project,
+        context,
+        workspaceRoot,
+        new TestSourceImportConfig(false),
+        decoder,
+        ImmutableList.of(new WorkspacePath("java/com/google")),
+        sourceArtifacts,
+        NO_MANIFESTS);
 
     issues.assertIssueContaining("No package name string found");
   }
@@ -717,32 +688,28 @@
         .addFile("/root/java/com/google/Bla3.java", "package com.google;\n public class Bla3 {}");
     List<SourceArtifact> sourceArtifacts =
         ImmutableList.of(
-            SourceArtifact.builder(LABEL)
+            SourceArtifact.builder(RuleKey.forPlainTarget(LABEL))
                 .setArtifactLocation(
                     ArtifactLocation.builder()
                         .setRelativePath("java/com/google/Bla.java")
-                        .setRootPath("/root")
                         .setIsSource(true))
                 .build(),
-            SourceArtifact.builder(LABEL)
+            SourceArtifact.builder(RuleKey.forPlainTarget(LABEL))
                 .setArtifactLocation(
                     ArtifactLocation.builder()
                         .setRelativePath("java/com/google/Bla2.java")
-                        .setRootPath("/root")
                         .setIsSource(true))
                 .build(),
-            SourceArtifact.builder(LABEL)
+            SourceArtifact.builder(RuleKey.forPlainTarget(LABEL))
                 .setArtifactLocation(
                     ArtifactLocation.builder()
                         .setRelativePath("java/com/google/Bla3.java")
-                        .setRootPath("/root")
                         .setIsSource(true))
                 .build(),
-            SourceArtifact.builder(LABEL)
+            SourceArtifact.builder(RuleKey.forPlainTarget(LABEL))
                 .setArtifactLocation(
                     ArtifactLocation.builder()
                         .setRelativePath("java/com/google/Foo.java")
-                        .setRootPath("/root")
                         .setIsSource(true))
                 .build());
     ImmutableList<BlazeContentEntry> result =
@@ -779,25 +746,22 @@
             "package com.google.subpackage.subsubpackage;\n public class Bla {}");
     List<SourceArtifact> sourceArtifacts =
         ImmutableList.of(
-            SourceArtifact.builder(LABEL)
+            SourceArtifact.builder(RuleKey.forPlainTarget(LABEL))
                 .setArtifactLocation(
                     ArtifactLocation.builder()
                         .setRelativePath("java/com/google/Bla.java")
-                        .setRootPath("/root")
                         .setIsSource(true))
                 .build(),
-            SourceArtifact.builder(LABEL)
+            SourceArtifact.builder(RuleKey.forPlainTarget(LABEL))
                 .setArtifactLocation(
                     ArtifactLocation.builder()
                         .setRelativePath("java/com/google/subpackage/Bla.java")
-                        .setRootPath("/root")
                         .setIsSource(true))
                 .build(),
-            SourceArtifact.builder(LABEL)
+            SourceArtifact.builder(RuleKey.forPlainTarget(LABEL))
                 .setArtifactLocation(
                     ArtifactLocation.builder()
                         .setRelativePath("java/com/google/subpackage/subsubpackage/Bla.java")
-                        .setRootPath("/root")
                         .setIsSource(true))
                 .build());
     ImmutableList<BlazeContentEntry> result =
@@ -836,18 +800,16 @@
             "package com.google.packagewrong1;\n public class Bla {}");
     List<SourceArtifact> sourceArtifacts =
         ImmutableList.of(
-            SourceArtifact.builder(LABEL)
+            SourceArtifact.builder(RuleKey.forPlainTarget(LABEL))
                 .setArtifactLocation(
                     ArtifactLocation.builder()
                         .setRelativePath("java/com/google/package0/Bla.java")
-                        .setRootPath("/root")
                         .setIsSource(true))
                 .build(),
-            SourceArtifact.builder(LABEL)
+            SourceArtifact.builder(RuleKey.forPlainTarget(LABEL))
                 .setArtifactLocation(
                     ArtifactLocation.builder()
                         .setRelativePath("java/com/google/package1/Bla.java")
-                        .setRootPath("/root")
                         .setIsSource(true))
                 .build());
     ImmutableList<BlazeContentEntry> result =
@@ -891,20 +853,18 @@
             "package com.google.android.chimera.container;\n public class FileApkUtils {}");
     List<SourceArtifact> sourceArtifacts =
         ImmutableList.of(
-            SourceArtifact.builder(LABEL)
+            SourceArtifact.builder(RuleKey.forPlainTarget(LABEL))
                 .setArtifactLocation(
                     ArtifactLocation.builder()
                         .setRelativePath(
                             "java/com/google/android/chimera/internal/Preconditions.java")
-                        .setRootPath("/root")
                         .setIsSource(true))
                 .build(),
-            SourceArtifact.builder(LABEL)
+            SourceArtifact.builder(RuleKey.forPlainTarget(LABEL))
                 .setArtifactLocation(
                     ArtifactLocation.builder()
                         .setRelativePath(
                             "java/com/google/android/chimera/container/FileApkUtils.java")
-                        .setRootPath("/root")
                         .setIsSource(true))
                 .build());
     ImmutableList<BlazeContentEntry> result =
@@ -942,21 +902,25 @@
                 .setIsSource(true)
                 .build()),
         ImmutableList.of("com.google"));
-    ImmutableMap<Label, ArtifactLocation> manifests =
-        ImmutableMap.<Label, ArtifactLocation>builder()
+    ImmutableMap<RuleKey, ArtifactLocation> manifests =
+        ImmutableMap.<RuleKey, ArtifactLocation>builder()
             .put(
-                LABEL,
+                RuleKey.forPlainTarget(LABEL),
                 ArtifactLocation.builder()
                     .setRelativePath("java/com/test.manifest")
-                    .setRootPath("/root")
                     .setIsSource(true)
                     .build())
             .build();
-    Map<Label, Map<String, String>> manifestMap =
+    Map<RuleKey, Map<ArtifactLocation, String>> manifestMap =
         readPackageManifestFiles(manifests, getDecoder("/root"));
 
-    assertThat(manifestMap.get(LABEL))
-        .containsEntry("/root/java/com/google/Bla.java", "com.google");
+    assertThat(manifestMap.get(RuleKey.forPlainTarget(LABEL)))
+        .containsEntry(
+            ArtifactLocation.builder()
+                .setRelativePath("java/com/google/Bla.java")
+                .setIsSource(true)
+                .build(),
+            "com.google");
   }
 
   @Test
@@ -965,21 +929,25 @@
         "/root/java/com/test.manifest",
         ImmutableList.of("java/com/google/Bla.java"),
         ImmutableList.of("com.google"));
-    ImmutableMap<Label, ArtifactLocation> manifests =
-        ImmutableMap.<Label, ArtifactLocation>builder()
+    ImmutableMap<RuleKey, ArtifactLocation> manifests =
+        ImmutableMap.<RuleKey, ArtifactLocation>builder()
             .put(
-                LABEL,
+                RuleKey.forPlainTarget(LABEL),
                 ArtifactLocation.builder()
                     .setRelativePath("java/com/test.manifest")
-                    .setRootPath("/root")
                     .setIsSource(true)
                     .build())
             .build();
-    Map<Label, Map<String, String>> manifestMap =
+    Map<RuleKey, Map<ArtifactLocation, String>> manifestMap =
         readPackageManifestFiles(manifests, getDecoder("/root"));
 
-    assertThat(manifestMap.get(LABEL))
-        .containsEntry("/root/java/com/google/Bla.java", "com.google");
+    assertThat(manifestMap.get(RuleKey.forPlainTarget(LABEL)))
+        .containsEntry(
+            ArtifactLocation.builder()
+                .setRelativePath("java/com/google/Bla.java")
+                .setIsSource(true)
+                .build(),
+            "com.google");
   }
 
   @Test
@@ -992,34 +960,47 @@
         "/root/java/com/test2.manifest",
         ImmutableList.of("java/com/google/Bla.java", "java/com/google/other/Temp.java"),
         ImmutableList.of("com.google", "com.google.other"));
-    ImmutableMap<Label, ArtifactLocation> manifests =
-        ImmutableMap.<Label, ArtifactLocation>builder()
+    ImmutableMap<RuleKey, ArtifactLocation> manifests =
+        ImmutableMap.<RuleKey, ArtifactLocation>builder()
             .put(
-                new Label("//a:a"),
+                RuleKey.forPlainTarget(new Label("//a:a")),
                 ArtifactLocation.builder()
                     .setRelativePath("java/com/test.manifest")
-                    .setRootPath("/root")
                     .setIsSource(true)
                     .build())
             .put(
-                new Label("//b:b"),
+                RuleKey.forPlainTarget(new Label("//b:b")),
                 ArtifactLocation.builder()
                     .setRelativePath("java/com/test2.manifest")
-                    .setRootPath("/root")
                     .setIsSource(true)
                     .build())
             .build();
-    Map<Label, Map<String, String>> manifestMap =
+    Map<RuleKey, Map<ArtifactLocation, String>> manifestMap =
         readPackageManifestFiles(manifests, getDecoder("/root"));
 
     assertThat(manifestMap).hasSize(2);
 
-    assertThat(manifestMap.get(new Label("//a:a")))
-        .containsEntry("/root/java/com/google/Bla.java", "com.google");
-    assertThat(manifestMap.get(new Label("//a:a")))
-        .containsEntry("/root/java/com/google/Foo.java", "com.google.subpackage");
-    assertThat(manifestMap.get(new Label("//b:b")))
-        .containsEntry("/root/java/com/google/other/Temp.java", "com.google.other");
+    assertThat(manifestMap.get(RuleKey.forPlainTarget(new Label("//a:a"))))
+        .containsEntry(
+            ArtifactLocation.builder()
+                .setRelativePath("java/com/google/Bla.java")
+                .setIsSource(true)
+                .build(),
+            "com.google");
+    assertThat(manifestMap.get(RuleKey.forPlainTarget(new Label("//a:a"))))
+        .containsEntry(
+            ArtifactLocation.builder()
+                .setRelativePath("java/com/google/Foo.java")
+                .setIsSource(true)
+                .build(),
+            "com.google.subpackage");
+    assertThat(manifestMap.get(RuleKey.forPlainTarget(new Label("//b:b"))))
+        .containsEntry(
+            ArtifactLocation.builder()
+                .setRelativePath("java/com/google/other/Temp.java")
+                .setIsSource(true)
+                .build(),
+            "com.google.other");
   }
 
   @Test
@@ -1033,38 +1014,34 @@
         "/root/java/com/google/subpackage/Bla.java",
         "package com.google.different;\n public class Bla {}");
 
-    ImmutableMap<Label, ArtifactLocation> manifests =
-        ImmutableMap.<Label, ArtifactLocation>builder()
+    ImmutableMap<RuleKey, ArtifactLocation> manifests =
+        ImmutableMap.<RuleKey, ArtifactLocation>builder()
             .put(
-                LABEL,
+                RuleKey.forPlainTarget(LABEL),
                 ArtifactLocation.builder()
                     .setRelativePath("java/com/test.manifest")
-                    .setRootPath("/root")
                     .setIsSource(true)
                     .build())
             .build();
 
     List<SourceArtifact> sourceArtifacts =
         ImmutableList.of(
-            SourceArtifact.builder(LABEL)
+            SourceArtifact.builder(RuleKey.forPlainTarget(LABEL))
                 .setArtifactLocation(
                     ArtifactLocation.builder()
                         .setRelativePath("java/com/google/Bla.java")
-                        .setRootPath("/root")
                         .setIsSource(true))
                 .build(),
-            SourceArtifact.builder(LABEL)
+            SourceArtifact.builder(RuleKey.forPlainTarget(LABEL))
                 .setArtifactLocation(
                     ArtifactLocation.builder()
                         .setRelativePath("java/com/google/Foo.java")
-                        .setRootPath("/root")
                         .setIsSource(true))
                 .build(),
-            SourceArtifact.builder(LABEL)
+            SourceArtifact.builder(RuleKey.forPlainTarget(LABEL))
                 .setArtifactLocation(
                     ArtifactLocation.builder()
                         .setRelativePath("java/com/google/subpackage/Bla.java")
-                        .setRootPath("/root")
                         .setIsSource(true))
                 .build());
 
@@ -1095,9 +1072,7 @@
   }
 
   private void setPackageManifest(
-      String manifestPath,
-      List<String> sourceRelativePaths,
-      List<String> packages) {
+      String manifestPath, List<String> sourceRelativePaths, List<String> packages) {
     PackageManifest.Builder manifest = PackageManifest.newBuilder();
     for (int i = 0; i < sourceRelativePaths.size(); i++) {
       String sourceRelativePath = sourceRelativePaths.get(i);
@@ -1137,7 +1112,8 @@
             ImmutableList.of(root),
             new ExecutionRootPath("out/crosstool/bin"),
             new ExecutionRootPath("out/crosstool/gen"));
-    return new ArtifactLocationDecoder(roots, new WorkspacePathResolverImpl(workspaceRoot, roots));
+    return new ArtifactLocationDecoderImpl(
+        roots, new WorkspacePathResolverImpl(workspaceRoot, roots));
   }
 
   private static class MockInputStreamProvider implements InputStreamProvider {
@@ -1169,8 +1145,8 @@
     }
   }
 
-  private Map<Label, Map<String, String>> readPackageManifestFiles(
-      Map<Label, ArtifactLocation> manifests, ArtifactLocationDecoder decoder) {
+  private Map<RuleKey, Map<ArtifactLocation, String>> readPackageManifestFiles(
+      Map<RuleKey, ArtifactLocation> manifests, ArtifactLocationDecoder decoder) {
     return PackageManifestReader.getInstance()
         .readPackageManifestFiles(
             project, context, decoder, manifests, MoreExecutors.sameThreadExecutor());
diff --git a/plugin_dev/BUILD b/plugin_dev/BUILD
index 0e90f5a..48eb552 100644
--- a/plugin_dev/BUILD
+++ b/plugin_dev/BUILD
@@ -55,7 +55,7 @@
 )
 
 load(
-    "//intellij_test:test_defs.bzl",
+    "//testing:test_defs.bzl",
     "intellij_integration_test_suite",
 )
 
@@ -74,5 +74,6 @@
         "//base:unit_test_utils",
         "//intellij_platform_sdk:plugin_api_for_tests",
         "@jsr305_annotations//jar",
+        "@junit//jar",
     ],
 )
diff --git a/plugin_dev/src/META-INF/blaze-plugin-dev.xml b/plugin_dev/src/META-INF/blaze-plugin-dev.xml
index 8725148..af9bc68 100644
--- a/plugin_dev/src/META-INF/blaze-plugin-dev.xml
+++ b/plugin_dev/src/META-INF/blaze-plugin-dev.xml
@@ -17,7 +17,7 @@
   <depends>DevKit</depends>
 
   <extensions defaultExtensionNs="com.google.idea.blaze">
-    <RuleConfigurationFactory implementation="com.google.idea.blaze.plugin.run.BlazeIntellijPluginConfigurationType$BlazeIntellijPluginRuleConfigurationFactory"/>
+    <RunConfigurationFactory implementation="com.google.idea.blaze.plugin.run.BlazeIntellijPluginConfigurationType$BlazeIntellijPluginRunConfigurationFactory"/>
     <SyncPlugin implementation="com.google.idea.blaze.plugin.sync.IntellijPluginSyncPlugin"/>
   </extensions>
 
diff --git a/plugin_dev/src/com/google/idea/blaze/plugin/run/BlazeIntellijPluginConfiguration.java b/plugin_dev/src/com/google/idea/blaze/plugin/run/BlazeIntellijPluginConfiguration.java
index d909032..57e0e9c 100644
--- a/plugin_dev/src/com/google/idea/blaze/plugin/run/BlazeIntellijPluginConfiguration.java
+++ b/plugin_dev/src/com/google/idea/blaze/plugin/run/BlazeIntellijPluginConfiguration.java
@@ -27,6 +27,7 @@
 import com.google.idea.blaze.base.ideinfo.JavaRuleIdeInfo;
 import com.google.idea.blaze.base.ideinfo.LibraryArtifact;
 import com.google.idea.blaze.base.ideinfo.RuleIdeInfo;
+import com.google.idea.blaze.base.model.BlazeProjectData;
 import com.google.idea.blaze.base.model.primitives.Label;
 import com.google.idea.blaze.base.model.primitives.TargetExpression;
 import com.google.idea.blaze.base.projectview.ProjectViewSet;
@@ -34,6 +35,8 @@
 import com.google.idea.blaze.base.run.BlazeRunConfiguration;
 import com.google.idea.blaze.base.run.rulefinder.RuleFinder;
 import com.google.idea.blaze.base.settings.Blaze;
+import com.google.idea.blaze.base.sync.data.BlazeProjectDataManager;
+import com.google.idea.blaze.base.sync.workspace.ArtifactLocationDecoder;
 import com.google.idea.blaze.base.ui.UiUtil;
 import com.google.idea.blaze.plugin.IntellijPluginRule;
 import com.intellij.execution.ExecutionException;
@@ -136,29 +139,42 @@
     this.target = target;
   }
 
+  public void setPluginSdk(Sdk sdk) {
+    if (IdeaJdkHelper.isIdeaJdk(sdk)) {
+      pluginSdk = sdk;
+    }
+  }
+
   private ImmutableList<File> findPluginJars() throws ExecutionException {
+    BlazeProjectData blazeProjectData =
+        BlazeProjectDataManager.getInstance(getProject()).getBlazeProjectData();
+    if (blazeProjectData == null) {
+      throw new ExecutionException("Not synced yet, please sync project");
+    }
     RuleIdeInfo rule = RuleFinder.getInstance().ruleForTarget(getProject(), getTarget());
     if (rule == null) {
       throw new ExecutionException(
           buildSystem + " rule '" + getTarget() + "' not imported during sync");
     }
     return IntellijPluginRule.isPluginBundle(rule)
-        ? findBundledJars(rule)
-        : ImmutableList.of(findPluginJar(rule));
+        ? findBundledJars(blazeProjectData.artifactLocationDecoder, rule)
+        : ImmutableList.of(findPluginJar(blazeProjectData.artifactLocationDecoder, rule));
   }
 
-  private ImmutableList<File> findBundledJars(RuleIdeInfo rule) throws ExecutionException {
+  private ImmutableList<File> findBundledJars(
+      ArtifactLocationDecoder artifactLocationDecoder, RuleIdeInfo rule) throws ExecutionException {
     ImmutableList.Builder<File> jars = ImmutableList.builder();
     for (Label dep : rule.dependencies) {
       RuleIdeInfo depRule = RuleFinder.getInstance().ruleForTarget(getProject(), dep);
       if (depRule != null && IntellijPluginRule.isSinglePluginRule(depRule)) {
-        jars.add(findPluginJar(depRule));
+        jars.add(findPluginJar(artifactLocationDecoder, depRule));
       }
     }
     return jars.build();
   }
 
-  private File findPluginJar(RuleIdeInfo rule) throws ExecutionException {
+  private File findPluginJar(ArtifactLocationDecoder artifactLocationDecoder, RuleIdeInfo rule)
+      throws ExecutionException {
     JavaRuleIdeInfo javaRuleIdeInfo = rule.javaRuleIdeInfo;
     if (!IntellijPluginRule.isSinglePluginRule(rule) || javaRuleIdeInfo == null) {
       throw new ExecutionException(
@@ -172,7 +188,7 @@
     if (artifact == null || artifact.classJar == null) {
       throw new ExecutionException("No output plugin jar found for '" + rule.label + "'");
     }
-    return artifact.classJar.getFile();
+    return artifactLocationDecoder.decode(artifact.classJar);
   }
 
   /**
diff --git a/plugin_dev/src/com/google/idea/blaze/plugin/run/BlazeIntellijPluginConfigurationType.java b/plugin_dev/src/com/google/idea/blaze/plugin/run/BlazeIntellijPluginConfigurationType.java
index 38baa96..5ff8d45 100644
--- a/plugin_dev/src/com/google/idea/blaze/plugin/run/BlazeIntellijPluginConfigurationType.java
+++ b/plugin_dev/src/com/google/idea/blaze/plugin/run/BlazeIntellijPluginConfigurationType.java
@@ -16,11 +16,13 @@
 package com.google.idea.blaze.plugin.run;
 
 import com.google.idea.blaze.base.ideinfo.RuleIdeInfo;
+import com.google.idea.blaze.base.ideinfo.RuleKey;
+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.WorkspaceType;
-import com.google.idea.blaze.base.run.BlazeRuleConfigurationFactory;
+import com.google.idea.blaze.base.run.BlazeRunConfigurationFactory;
 import com.google.idea.blaze.base.run.rulefinder.RuleFinder;
 import com.google.idea.blaze.base.settings.Blaze;
-import com.google.idea.blaze.base.sync.projectview.WorkspaceLanguageSettings;
 import com.google.idea.blaze.plugin.IntellijPluginRule;
 import com.intellij.diagnostic.VMOptions;
 import com.intellij.execution.BeforeRunTask;
@@ -30,6 +32,8 @@
 import com.intellij.execution.configurations.RunConfiguration;
 import com.intellij.icons.AllIcons;
 import com.intellij.openapi.project.Project;
+import com.intellij.openapi.projectRoots.Sdk;
+import com.intellij.openapi.roots.ProjectRootManager;
 import com.intellij.openapi.util.Key;
 import com.intellij.openapi.util.NullableLazyValue;
 import javax.annotation.Nullable;
@@ -44,12 +48,15 @@
   private final BlazeIntellijPluginConfigurationFactory factory =
       new BlazeIntellijPluginConfigurationFactory(this);
 
-  static class BlazeIntellijPluginRuleConfigurationFactory extends BlazeRuleConfigurationFactory {
+  static class BlazeIntellijPluginRunConfigurationFactory extends BlazeRunConfigurationFactory {
     @Override
-    public boolean handlesRule(
-        WorkspaceLanguageSettings workspaceLanguageSettings, RuleIdeInfo rule) {
-      return workspaceLanguageSettings.isWorkspaceType(WorkspaceType.INTELLIJ_PLUGIN)
-          && IntellijPluginRule.isPluginRule(rule);
+    public boolean handlesTarget(Project project, BlazeProjectData blazeProjectData, Label target) {
+      if (!blazeProjectData.workspaceLanguageSettings.isWorkspaceType(
+          WorkspaceType.INTELLIJ_PLUGIN)) {
+        return false;
+      }
+      RuleIdeInfo rule = blazeProjectData.ruleMap.get(RuleKey.forPlainTarget(target));
+      return rule != null && IntellijPluginRule.isPluginRule(rule);
     }
 
     @Override
@@ -58,10 +65,10 @@
     }
 
     @Override
-    public void setupConfiguration(RunConfiguration configuration, RuleIdeInfo rule) {
+    public void setupConfiguration(RunConfiguration configuration, Label target) {
       final BlazeIntellijPluginConfiguration pluginConfig =
           (BlazeIntellijPluginConfiguration) configuration;
-      getInstance().factory.setupConfigurationForRule(pluginConfig, rule);
+      getInstance().factory.setupConfigurationForRule(pluginConfig, target);
     }
   }
 
@@ -103,13 +110,16 @@
       task.setEnabled(providerID.equals(BuildPluginBeforeRunTaskProvider.ID));
     }
 
-    void setupConfigurationForRule(
-        BlazeIntellijPluginConfiguration configuration, RuleIdeInfo rule) {
-      configuration.setTarget(rule.label);
+    void setupConfigurationForRule(BlazeIntellijPluginConfiguration configuration, Label target) {
+      configuration.setTarget(target);
       configuration.setGeneratedName();
       if (configuration.vmParameters == null) {
         configuration.vmParameters = currentVmOptions.getValue();
       }
+      Sdk projectSdk = ProjectRootManager.getInstance(configuration.getProject()).getProjectSdk();
+      if (IdeaJdkHelper.isIdeaJdk(projectSdk)) {
+        configuration.setPluginSdk(projectSdk);
+      }
     }
 
     @Override
diff --git a/plugin_dev/tests/integrationtests/com/google/idea/blaze/plugin/sync/PluginDevSyncTest.java b/plugin_dev/tests/integrationtests/com/google/idea/blaze/plugin/sync/PluginDevSyncTest.java
index 492a6fb..9515601 100644
--- a/plugin_dev/tests/integrationtests/com/google/idea/blaze/plugin/sync/PluginDevSyncTest.java
+++ b/plugin_dev/tests/integrationtests/com/google/idea/blaze/plugin/sync/PluginDevSyncTest.java
@@ -18,9 +18,9 @@
 import static com.google.common.truth.Truth.assertThat;
 
 import com.google.idea.blaze.base.ideinfo.RuleIdeInfo;
+import com.google.idea.blaze.base.ideinfo.RuleMap;
 import com.google.idea.blaze.base.ideinfo.RuleMapBuilder;
 import com.google.idea.blaze.base.model.BlazeProjectData;
-import com.google.idea.blaze.base.model.RuleMap;
 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;
@@ -30,10 +30,15 @@
 import com.intellij.execution.RunManager;
 import com.intellij.execution.configurations.RunConfiguration;
 import java.util.List;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
 
 /** Plugin-dev specific sync integration test. */
+@RunWith(JUnit4.class)
 public class PluginDevSyncTest extends BlazeSyncIntegrationTestCase {
 
+  @Test
   public void testRunConfigurationCreatedDuringSync() throws Exception {
     setProjectView(
         "directories:",
diff --git a/proto_deps/proto_deps.jar b/proto_deps/proto_deps.jar
index 80b6046..25a6ec4 100755
--- a/proto_deps/proto_deps.jar
+++ b/proto_deps/proto_deps.jar
Binary files differ
diff --git a/intellij_test/BUILD b/testing/BUILD
similarity index 100%
rename from intellij_test/BUILD
rename to testing/BUILD
diff --git a/intellij_test/src/com/google/idea/blaze/base/BlazeTestSystemProperties.java b/testing/src/com/google/idea/testing/BlazeTestSystemPropertiesRule.java
similarity index 75%
rename from intellij_test/src/com/google/idea/blaze/base/BlazeTestSystemProperties.java
rename to testing/src/com/google/idea/testing/BlazeTestSystemPropertiesRule.java
index c8a3351..f8d7f19 100644
--- a/intellij_test/src/com/google/idea/blaze/base/BlazeTestSystemProperties.java
+++ b/testing/src/com/google/idea/testing/BlazeTestSystemPropertiesRule.java
@@ -13,10 +13,12 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package com.google.idea.blaze.base;
+package com.google.idea.testing;
 
 import com.google.common.base.Joiner;
 import com.google.common.collect.Lists;
+import com.google.common.io.Files;
+import com.intellij.ide.plugins.PluginManagerCore;
 import com.intellij.openapi.application.Application;
 import com.intellij.openapi.application.PathManager;
 import com.intellij.openapi.vfs.newvfs.impl.VfsRootAccess;
@@ -24,25 +26,28 @@
 import java.io.File;
 import java.io.IOException;
 import java.net.URL;
+import java.nio.charset.StandardCharsets;
 import java.util.Enumeration;
 import java.util.List;
 import javax.annotation.Nullable;
+import org.junit.rules.ExternalResource;
 
-/** Test utilities specific to running in a blaze/bazel environment. */
-public class BlazeTestSystemProperties {
+/**
+ * Test utilities specific to running IntelliJ integration tests in a blaze/bazel environment.
+ * Should be instantiated as a @ClassRule in the outermost test class/suite.
+ */
+public class BlazeTestSystemPropertiesRule extends ExternalResource {
+
+  @Override
+  protected void before() throws Throwable {
+    configureSystemProperties();
+  }
 
   /** The absolute path to the runfiles directory. */
   private static final String RUNFILES_PATH = getUserValue("TEST_SRCDIR");
 
-  public static boolean isRunThroughBlaze() {
-    return System.getenv("JAVA_RUNFILES") != null;
-  }
-
   /** Sets up the necessary system properties for running IntelliJ tests via blaze/bazel. */
-  public static void configureSystemProperties() throws IOException {
-    if (!isRunThroughBlaze()) {
-      return;
-    }
+  private static void configureSystemProperties() throws IOException {
     File sandbox = new File(getTmpDirFile(), "_intellij_test_sandbox");
 
     setSandboxPath("idea.home.path", new File(sandbox, "home"));
@@ -51,6 +56,10 @@
     setIfEmpty(PlatformUtils.PLATFORM_PREFIX_KEY, "Idea");
     setIfEmpty("idea.classpath.index.enabled", "false");
 
+    // Some plugins have a since-build and until-build restriction, so we need
+    // to update the build number here
+    PluginManagerCore.BUILD_NUMBER = readApiVersionNumber();
+
     // Tests fail if they access files outside of the project roots and other system directories.
     // Ensure runfiles and platform api are whitelisted.
     VfsRootAccess.allowRootAccess(RUNFILES_PATH);
@@ -62,7 +71,7 @@
     List<String> pluginJars = Lists.newArrayList();
     try {
       Enumeration<URL> urls =
-          BlazeTestSystemProperties.class.getClassLoader().getResources("META-INF/plugin.xml");
+          BlazeTestSystemPropertiesRule.class.getClassLoader().getResources("META-INF/plugin.xml");
       while (urls.hasMoreElements()) {
         URL url = urls.nextElement();
         addArchiveFile(url, pluginJars);
@@ -75,6 +84,26 @@
     setIfEmpty("idea.plugins.path", Joiner.on(File.pathSeparator).join(pluginJars));
   }
 
+  private static String readApiVersionNumber() {
+    String apiVersionFilePath = System.getProperty("blaze.idea.api.version.file");
+    String runfilesWorkspaceRoot = System.getProperty("user.dir");
+    if (apiVersionFilePath == null) {
+      throw new RuntimeException("No api_version_file found in runfiles directory");
+    }
+    if (runfilesWorkspaceRoot == null) {
+      throw new RuntimeException("Runfiles workspace root not found");
+    }
+    File apiVersionFile = new File(runfilesWorkspaceRoot, apiVersionFilePath);
+    if (!apiVersionFile.canRead()) {
+      return null;
+    }
+    try {
+      return Files.readFirstLine(apiVersionFile, StandardCharsets.UTF_8);
+    } catch (IOException e) {
+      throw new RuntimeException(e);
+    }
+  }
+
   @Nullable
   private static String getPlatformApiPath() {
     String platformJar = PathManager.getJarPathForClass(Application.class);
diff --git a/testing/src/com/google/idea/testing/EdtRule.java b/testing/src/com/google/idea/testing/EdtRule.java
new file mode 100644
index 0000000..4f374d2
--- /dev/null
+++ b/testing/src/com/google/idea/testing/EdtRule.java
@@ -0,0 +1,38 @@
+/*
+ * Copyright 2016 The Bazel Authors. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.idea.testing;
+
+import com.intellij.testFramework.EdtTestUtil;
+import com.intellij.testFramework.TestRunnerUtil;
+import com.intellij.util.ThrowableRunnable;
+import org.junit.rules.TestRule;
+import org.junit.runner.Description;
+import org.junit.runners.model.Statement;
+
+/** Rule to run tests on the EDT. */
+public class EdtRule implements TestRule {
+
+  @Override
+  public Statement apply(Statement base, Description description) {
+    return new Statement() {
+      @Override
+      public void evaluate() throws Throwable {
+        TestRunnerUtil.replaceIdeEventQueueSafely();
+        EdtTestUtil.runInEdtAndWait((ThrowableRunnable<Throwable>) base::evaluate);
+      }
+    };
+  }
+}
diff --git a/testing/src/com/google/idea/testing/IntellijTestSetupRule.java b/testing/src/com/google/idea/testing/IntellijTestSetupRule.java
new file mode 100644
index 0000000..4f9252c
--- /dev/null
+++ b/testing/src/com/google/idea/testing/IntellijTestSetupRule.java
@@ -0,0 +1,130 @@
+/*
+ * Copyright 2016 The Bazel Authors. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.idea.testing;
+
+import com.intellij.openapi.Disposable;
+import com.intellij.openapi.application.PathManager;
+import com.intellij.openapi.util.Disposer;
+import com.intellij.openapi.util.io.FileSystemUtil;
+import com.intellij.util.ReflectionUtil;
+import com.intellij.util.ui.UIUtil;
+import java.io.File;
+import java.util.Hashtable;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.stream.Collectors;
+import org.junit.rules.ExternalResource;
+
+/**
+ * Runs before and after each test, performing the checks in {@link
+ * com.intellij.testFramework.UsefulTestCase}
+ */
+public class IntellijTestSetupRule extends ExternalResource {
+
+  private static final Set<?> DELETE_ON_EXIT_HOOK_DOT_FILES;
+  private static final Class<?> DELETE_ON_EXIT_HOOK_CLASS;
+
+  static {
+    // Radar #5755208: Command line Java applications need a way to launch without a Dock icon.
+    System.setProperty("apple.awt.UIElement", "true");
+
+    try {
+      Class<?> aClass = Class.forName("java.io.DeleteOnExitHook");
+      Set<?> files = ReflectionUtil.getStaticFieldValue(aClass, Set.class, "files");
+      DELETE_ON_EXIT_HOOK_CLASS = aClass;
+      DELETE_ON_EXIT_HOOK_DOT_FILES = files;
+
+    } catch (Exception e) {
+      throw new RuntimeException(e);
+    }
+  }
+
+  public Disposable testRootDisposable;
+
+  private String oldPluginPathProperty;
+
+  @Override
+  protected void before() throws Throwable {
+    if (!isRunThroughBlaze()) {
+      // If running directly through the IDE, don't try to load plugins from the sandbox environment
+      // Instead we'll rely on the slightly more hermetic module classpath
+      oldPluginPathProperty = System.getProperty(PathManager.PROPERTY_PLUGINS_PATH);
+      System.setProperty(PathManager.PROPERTY_PLUGINS_PATH, "/dev/null");
+    }
+    testRootDisposable = Disposer.newDisposable();
+  }
+
+  @Override
+  protected void after() {
+    if (oldPluginPathProperty != null) {
+      System.setProperty(PathManager.PROPERTY_PLUGINS_PATH, oldPluginPathProperty);
+    } else {
+      System.clearProperty(PathManager.PROPERTY_PLUGINS_PATH);
+    }
+    try {
+      Disposer.dispose(testRootDisposable);
+      cleanupSwingDataStructures();
+      cleanupDeleteOnExitHookList();
+    } catch (Throwable e) {
+      throw new RuntimeException(e);
+    }
+
+    UIUtil.removeLeakingAppleListeners();
+  }
+
+  private static boolean isRunThroughBlaze() {
+    return System.getenv("JAVA_RUNFILES") != null;
+  }
+
+  private static void cleanupSwingDataStructures() throws Exception {
+    Object manager =
+        ReflectionUtil.getDeclaredMethod(
+                Class.forName("javax.swing.KeyboardManager"), "getCurrentManager")
+            .invoke(null);
+    Map<?, ?> componentKeyStrokeMap =
+        ReflectionUtil.getField(
+            manager.getClass(), manager, Hashtable.class, "componentKeyStrokeMap");
+    componentKeyStrokeMap.clear();
+    Map<?, ?> containerMap =
+        ReflectionUtil.getField(manager.getClass(), manager, Hashtable.class, "containerMap");
+    containerMap.clear();
+  }
+
+  private static void cleanupDeleteOnExitHookList() throws Exception {
+    // try to reduce file set retained by java.io.DeleteOnExitHook
+    List<String> list;
+    synchronized (DELETE_ON_EXIT_HOOK_CLASS) {
+      if (DELETE_ON_EXIT_HOOK_DOT_FILES.isEmpty()) {
+        return;
+      }
+      list =
+          DELETE_ON_EXIT_HOOK_DOT_FILES
+              .stream()
+              .filter(p -> p instanceof String)
+              .map(p -> (String) p)
+              .collect(Collectors.toList());
+    }
+    for (int i = list.size() - 1; i >= 0; i--) {
+      String path = list.get(i);
+      if (FileSystemUtil.getAttributes(path) == null || new File(path).delete()) {
+        synchronized (DELETE_ON_EXIT_HOOK_CLASS) {
+          DELETE_ON_EXIT_HOOK_DOT_FILES.remove(path);
+        }
+      }
+    }
+  }
+}
diff --git a/testing/src/com/google/idea/testing/ServiceHelper.java b/testing/src/com/google/idea/testing/ServiceHelper.java
new file mode 100644
index 0000000..6b76692
--- /dev/null
+++ b/testing/src/com/google/idea/testing/ServiceHelper.java
@@ -0,0 +1,66 @@
+/*
+ * 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.testing;
+
+import com.intellij.openapi.Disposable;
+import com.intellij.openapi.application.ApplicationManager;
+import com.intellij.openapi.extensions.ExtensionPoint;
+import com.intellij.openapi.extensions.ExtensionPointName;
+import com.intellij.openapi.extensions.Extensions;
+import com.intellij.openapi.project.Project;
+import com.intellij.openapi.util.Disposer;
+import org.picocontainer.MutablePicoContainer;
+
+/** Utility class for registering project services, application services and extensions. */
+public class ServiceHelper {
+
+  public static <T> void registerExtension(
+      ExtensionPointName<T> name, T instance, Disposable parentDisposable) {
+    ExtensionPoint<T> ep = Extensions.getRootArea().getExtensionPoint(name);
+    ep.registerExtension(instance);
+    Disposer.register(parentDisposable, () -> ep.unregisterExtension(instance));
+  }
+
+  public static <T> void registerApplicationService(
+      Class<T> key, T implementation, Disposable parentDisposable) {
+    registerComponentInstance(
+        (MutablePicoContainer) ApplicationManager.getApplication().getPicoContainer(),
+        key,
+        implementation,
+        parentDisposable);
+  }
+
+  public static <T> void registerProjectService(
+      Project project, Class<T> key, T implementation, Disposable parentDisposable) {
+    registerComponentInstance(
+        (MutablePicoContainer) project.getPicoContainer(), key, implementation, parentDisposable);
+  }
+
+  private static <T> void registerComponentInstance(
+      MutablePicoContainer container, Class<T> key, T implementation, Disposable parentDisposable) {
+    Object old = container.getComponentInstance(key);
+    container.unregisterComponent(key.getName());
+    container.registerComponentInstance(key.getName(), implementation);
+    Disposer.register(
+        parentDisposable,
+        () -> {
+          container.unregisterComponent(key.getName());
+          if (old != null) {
+            container.registerComponentInstance(key.getName(), old);
+          }
+        });
+  }
+}
diff --git a/testing/test_defs.bzl b/testing/test_defs.bzl
new file mode 100644
index 0000000..38baa7c
--- /dev/null
+++ b/testing/test_defs.bzl
@@ -0,0 +1,166 @@
+"""Custom rule for creating IntelliJ plugin tests.
+"""
+
+load(
+    "//build_defs:build_defs.bzl",
+    "api_version_txt",
+)
+
+def intellij_unit_test_suite(name, srcs, test_package_root, **kwargs):
+  """Creates a java_test rule comprising all valid test classes in the specified srcs.
+
+  Only classes ending in "Test.java" will be recognized.
+
+  Args:
+    name: name of this rule.
+    srcs: the test classes.
+    test_package_root: only tests under this package root will be run.
+    **kwargs: Any other args to be passed to the java_test.
+  """
+  test_srcs = [test for test in srcs if test.endswith("Test.java")]
+  test_classes = [_get_test_class(test_src, test_package_root) for test_src in test_srcs]
+  suite_class_name = name + "TestSuite"
+  suite_class = test_package_root + "." + suite_class_name
+  _generate_test_suite(
+      name = suite_class_name,
+      test_package_root = test_package_root,
+      test_classes = test_classes,
+  )
+  native.java_test(
+      name = name,
+      srcs = srcs + [suite_class_name],
+      test_class = suite_class,
+      **kwargs)
+
+def intellij_integration_test_suite(
+    name,
+    srcs,
+    test_package_root,
+    deps,
+    jvm_flags = [],
+    runtime_deps = [],
+    platform_prefix="Idea",
+    required_plugins=None,
+    **kwargs):
+  """Creates a java_test rule comprising all valid test classes in the specified srcs.
+
+  Only classes ending in "Test.java" will be recognized.
+
+  All test classes must be located in the blaze package calling this function.
+
+  Args:
+    name: name of this rule.
+    srcs: the test classes.
+    test_package_root: only tests under this package root will be run.
+    deps: the required deps.
+    jvm_flags: extra flags to be passed to the test vm.
+    runtime_deps: the required runtime_deps.
+    platform_prefix: Specifies the JetBrains product these tests are run against. Examples are
+        'Idea' (IJ CE), 'idea' (IJ UE), 'CLion', 'AndroidStudio'. See
+        com.intellij.util.PlatformUtils for other options.
+    required_plugins: optional comma-separated list of plugin IDs. Integration tests will fail if
+        these plugins aren't loaded at runtime.
+    **kwargs: Any other args to be passed to the java_test.
+  """
+  test_srcs = [test for test in srcs if test.endswith("Test.java")]
+  test_classes = [_get_test_class(test_src, test_package_root) for test_src in test_srcs]
+  suite_class_name = name + "TestSuite"
+  suite_class = test_package_root + "." + suite_class_name
+  _generate_test_suite(
+      name = suite_class_name,
+      test_package_root = test_package_root,
+      test_classes = test_classes,
+      class_rules = ["com.google.idea.testing.BlazeTestSystemPropertiesRule"]
+  )
+
+  api_version_txt_name = name + "_api_version"
+  api_version_txt(name = api_version_txt_name)
+  data = kwargs.pop("data", [])
+  data.append(api_version_txt_name)
+
+  deps = list(deps)
+  deps.extend([
+      "//testing:lib",
+  ])
+  runtime_deps = list(runtime_deps)
+  runtime_deps.extend([
+      "//intellij_platform_sdk:bundled_plugins",
+      "//third_party:jdk8_tools",
+  ])
+
+  jvm_flags = list(jvm_flags)
+  jvm_flags.extend([
+      "-Didea.classpath.index.enabled=false",
+      "-Djava.awt.headless=true",
+      "-Didea.platform.prefix=" + platform_prefix,
+      "-Dblaze.idea.api.version.file=$(location %s)" % api_version_txt_name
+  ])
+
+  if required_plugins:
+    jvm_flags.append("-Didea.required.plugins.id=" + required_plugins)
+
+  native.java_test(
+      name = name,
+      size = "medium",
+      srcs = srcs + [suite_class_name],
+      data = data,
+      jvm_flags = jvm_flags,
+      test_class = suite_class,
+      runtime_deps = runtime_deps,
+      deps = deps,
+      **kwargs
+  )
+
+def _generate_test_suite(name, test_package_root, test_classes, class_rules = []):
+  """Generates a JUnit4 test suite pulling in all the referenced classes.
+
+  Args:
+    name: the name of the genrule and output test suite class.
+    test_package_root: the package string of the output test suite.
+    test_classes: the test classes included in the suite.
+    class_rules: optional list of classes to instantiate as a @ClassRule in the test suite.
+  """
+  lines = []
+  lines.append("package %s;" % test_package_root)
+  lines.append("")
+  if (class_rules):
+    lines.append("import org.junit.ClassRule;")
+  lines.append("import org.junit.runner.RunWith;")
+  lines.append("import org.junit.runners.Suite;")
+  lines.append("")
+  for test_class in test_classes:
+    lines.append("import %s;" % test_class)
+  lines.append("")
+  lines.append("@RunWith(Suite.class)")
+  lines.append("@Suite.SuiteClasses({")
+  for test_class in test_classes:
+    lines.append("    %s.class," % test_class.split(".")[-1])
+  lines.append("})")
+  lines.append("public class %s {" % name)
+  lines.append("")
+
+  i = 1
+  for class_rule in class_rules:
+    lines.append("@ClassRule")
+    lines.append("public static %s setupRule_%d = new %s();" % (class_rule, i, class_rule))
+    i += 1
+
+  lines.append("}")
+
+  contents = "\\n".join(lines)
+  native.genrule(
+      name = name,
+      cmd = "printf '%s' > $@" % contents,
+      outs = [name + ".java"],
+  )
+
+def _get_test_class(test_src, test_package_root):
+  """Returns the package string of the test class, beginning with the given root."""
+  full_path = PACKAGE_NAME + "/" + test_src
+  temp = full_path[:-5]
+  temp = temp.replace("/", ".")
+  i = temp.rfind(test_package_root)
+  if i < 0:
+    fail("Test source '%s' not under package root '%s'" % (full_path, test_package_root))
+  test_class = temp[i:]
+  return test_class
diff --git a/version.bzl b/version.bzl
index 96c8141..5cdb6a6 100644
--- a/version.bzl
+++ b/version.bzl
@@ -1,3 +1,3 @@
 """Version of the blaze plugin."""
 
-VERSION = "1.9.2"
+VERSION = "1.9.4"