Import of bazel plugin using copybara

PiperOrigin-RevId: 148774484
diff --git a/aswb/2.3/src/com/google/idea/blaze/android/project/BlazeFeatureEnableService.java b/aswb/2.3/src/com/google/idea/blaze/android/project/BlazeFeatureEnableService.java
index e59f375..2ec4852 100644
--- a/aswb/2.3/src/com/google/idea/blaze/android/project/BlazeFeatureEnableService.java
+++ b/aswb/2.3/src/com/google/idea/blaze/android/project/BlazeFeatureEnableService.java
@@ -16,7 +16,9 @@
 package com.google.idea.blaze.android.project;
 
 import com.android.tools.idea.project.FeatureEnableService;
+import com.google.common.collect.ImmutableMap;
 import com.google.idea.blaze.android.settings.BlazeAndroidUserSettings;
+import com.google.idea.blaze.base.logging.EventLogger;
 import com.google.idea.blaze.base.settings.Blaze;
 import com.google.idea.blaze.base.sync.data.BlazeProjectDataManager;
 import com.google.idea.common.experiments.BoolExperiment;
@@ -24,6 +26,8 @@
 
 /** Enable features supported by the blaze integration. */
 public class BlazeFeatureEnableService extends FeatureEnableService {
+  private static final EventLogger logger = EventLogger.getInstance();
+
   private static final BoolExperiment ENABLE_LAYOUT_EDITOR =
       new BoolExperiment("enable.layout.editor", true);
 
@@ -34,10 +38,12 @@
 
   @Override
   public boolean isLayoutEditorEnabled(Project project) {
-    return isLayoutEditorExperimentEnabled()
-        && BlazeAndroidUserSettings.getInstance().getUseLayoutEditor()
-        // Can't render if we don't have the data ready.
-        && BlazeProjectDataManager.getInstance(project).getBlazeProjectData() != null;
+    boolean isEnabled =
+        isLayoutEditorExperimentEnabled()
+            && BlazeAndroidUserSettings.getInstance().getUseLayoutEditor();
+    boolean isReady = BlazeProjectDataManager.getInstance(project).getBlazeProjectData() != null;
+    logger.log("layout_editor", ImmutableMap.of("enabled", Boolean.toString(isEnabled)));
+    return isEnabled && isReady;
   }
 
   public static boolean isLayoutEditorExperimentEnabled() {
diff --git a/aswb/2.3/tests/unittests/com/google/idea/blaze/android/project/BlazeFeatureEnabledServiceTest.java b/aswb/2.3/tests/unittests/com/google/idea/blaze/android/project/BlazeFeatureEnabledServiceTest.java
index a33eb31..8c9ccc7 100644
--- a/aswb/2.3/tests/unittests/com/google/idea/blaze/android/project/BlazeFeatureEnabledServiceTest.java
+++ b/aswb/2.3/tests/unittests/com/google/idea/blaze/android/project/BlazeFeatureEnabledServiceTest.java
@@ -23,6 +23,7 @@
 import com.google.common.collect.ImmutableList;
 import com.google.idea.blaze.android.settings.BlazeAndroidUserSettings;
 import com.google.idea.blaze.base.BlazeTestCase;
+import com.google.idea.blaze.base.logging.EventLogger;
 import com.google.idea.blaze.base.model.BlazeProjectData;
 import com.google.idea.blaze.base.settings.Blaze.BuildSystem;
 import com.google.idea.blaze.base.settings.BlazeImportSettings;
@@ -63,6 +64,8 @@
         new BlazeImportSettings(null, null, null, null, null, BuildSystem.Blaze));
     projectServices.register(BlazeImportSettingsManager.class, importSettingsManager);
 
+    registerExtensionPoint(
+        ExtensionPointName.create("com.google.idea.blaze.EventLogger"), EventLogger.class);
     ExtensionPoint<FeatureEnableService> extensionPoint =
         registerExtensionPoint(
             ExtensionPointName.create("com.android.project.featureEnableService"),
diff --git a/aswb/src/com/google/idea/blaze/android/run/binary/mobileinstall/BlazeApkBuildStepMobileInstall.java b/aswb/src/com/google/idea/blaze/android/run/binary/mobileinstall/BlazeApkBuildStepMobileInstall.java
index 2082b77..2e69c17 100644
--- a/aswb/src/com/google/idea/blaze/android/run/binary/mobileinstall/BlazeApkBuildStepMobileInstall.java
+++ b/aswb/src/com/google/idea/blaze/android/run/binary/mobileinstall/BlazeApkBuildStepMobileInstall.java
@@ -32,10 +32,10 @@
 import com.google.idea.blaze.android.sync.model.BlazeAndroidSyncData;
 import com.google.idea.blaze.base.async.executor.BlazeExecutor;
 import com.google.idea.blaze.base.async.process.ExternalTask;
-import com.google.idea.blaze.base.async.process.LineProcessingOutputStream;
 import com.google.idea.blaze.base.command.BlazeCommand;
 import com.google.idea.blaze.base.command.BlazeCommandName;
 import com.google.idea.blaze.base.command.BlazeFlags;
+import com.google.idea.blaze.base.command.buildresult.BuildResultHelper;
 import com.google.idea.blaze.base.filecache.FileCaches;
 import com.google.idea.blaze.base.issueparser.IssueOutputLineProcessor;
 import com.google.idea.blaze.base.model.BlazeProjectData;
@@ -118,13 +118,14 @@
             }
             WorkspaceRoot workspaceRoot = WorkspaceRoot.fromProject(project);
 
+            BlazeApkDeployInfoProtoHelper deployInfoHelper =
+                new BlazeApkDeployInfoProtoHelper(project, buildFlags);
+            BuildResultHelper buildResultHelper = deployInfoHelper.getBuildResultHelper();
+
             command
                 .addTargets(label)
                 .addBlazeFlags(buildFlags)
-                .addBlazeFlags(BlazeFlags.EXPERIMENTAL_SHOW_ARTIFACTS);
-
-            BlazeApkDeployInfoProtoHelper deployInfoHelper =
-                new BlazeApkDeployInfoProtoHelper(project, buildFlags);
+                .addBlazeFlags(buildResultHelper.getBuildFlags());
 
             SaveUtil.saveAllFiles();
             int retVal =
@@ -132,8 +133,7 @@
                     .addBlazeCommand(command.build())
                     .context(context)
                     .stderr(
-                        LineProcessingOutputStream.of(
-                            deployInfoHelper.getLineProcessor(),
+                        buildResultHelper.stderr(
                             new IssueOutputLineProcessor(project, context, workspaceRoot)))
                     .build()
                     .run();
diff --git a/aswb/src/com/google/idea/blaze/android/run/deployinfo/BlazeApkDeployInfoProtoHelper.java b/aswb/src/com/google/idea/blaze/android/run/deployinfo/BlazeApkDeployInfoProtoHelper.java
index ffe8c6e..4de0387 100644
--- a/aswb/src/com/google/idea/blaze/android/run/deployinfo/BlazeApkDeployInfoProtoHelper.java
+++ b/aswb/src/com/google/idea/blaze/android/run/deployinfo/BlazeApkDeployInfoProtoHelper.java
@@ -17,11 +17,9 @@
 
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.Iterables;
-import com.google.common.collect.Lists;
 import com.google.common.util.concurrent.ListenableFuture;
 import com.google.idea.blaze.android.manifest.ManifestParser;
-import com.google.idea.blaze.base.async.process.LineProcessingOutputStream;
-import com.google.idea.blaze.base.command.ExperimentalShowArtifactsLineProcessor;
+import com.google.idea.blaze.base.command.buildresult.BuildResultHelper;
 import com.google.idea.blaze.base.command.info.BlazeInfo;
 import com.google.idea.blaze.base.model.primitives.WorkspaceRoot;
 import com.google.idea.blaze.base.scope.BlazeContext;
@@ -44,24 +42,23 @@
   private final Project project;
   private final WorkspaceRoot workspaceRoot;
   private final ImmutableList<String> buildFlags;
-  private final List<File> deployInfoFiles = Lists.newArrayList();
-  private final LineProcessingOutputStream.LineProcessor lineProcessor =
-      new ExperimentalShowArtifactsLineProcessor(
-          deployInfoFiles, fileName -> fileName.endsWith(".deployinfo.pb"));
+  private final BuildResultHelper buildResultHelper;
 
   public BlazeApkDeployInfoProtoHelper(Project project, ImmutableList<String> buildFlags) {
     this.project = project;
     this.buildFlags = buildFlags;
     this.workspaceRoot = WorkspaceRoot.fromProject(project);
+    this.buildResultHelper =
+        BuildResultHelper.forFiles(fileName -> fileName.endsWith(".deployinfo.pb"));
   }
 
-  public LineProcessingOutputStream.LineProcessor getLineProcessor() {
-    return lineProcessor;
+  public BuildResultHelper getBuildResultHelper() {
+    return buildResultHelper;
   }
 
   @Nullable
   public BlazeAndroidDeployInfo readDeployInfo(BlazeContext context) {
-    File deployInfoFile = Iterables.getOnlyElement(deployInfoFiles, null);
+    File deployInfoFile = Iterables.getOnlyElement(buildResultHelper.getBuildArtifacts(), null);
     if (deployInfoFile == null) {
       return null;
     }
diff --git a/aswb/src/com/google/idea/blaze/android/run/runner/BlazeApkBuildStepNormalBuild.java b/aswb/src/com/google/idea/blaze/android/run/runner/BlazeApkBuildStepNormalBuild.java
index 1029f85..58daf4f 100644
--- a/aswb/src/com/google/idea/blaze/android/run/runner/BlazeApkBuildStepNormalBuild.java
+++ b/aswb/src/com/google/idea/blaze/android/run/runner/BlazeApkBuildStepNormalBuild.java
@@ -23,10 +23,9 @@
 import com.google.idea.blaze.android.run.deployinfo.BlazeApkDeployInfoProtoHelper;
 import com.google.idea.blaze.base.async.executor.BlazeExecutor;
 import com.google.idea.blaze.base.async.process.ExternalTask;
-import com.google.idea.blaze.base.async.process.LineProcessingOutputStream;
 import com.google.idea.blaze.base.command.BlazeCommand;
 import com.google.idea.blaze.base.command.BlazeCommandName;
-import com.google.idea.blaze.base.command.BlazeFlags;
+import com.google.idea.blaze.base.command.buildresult.BuildResultHelper;
 import com.google.idea.blaze.base.filecache.FileCaches;
 import com.google.idea.blaze.base.issueparser.IssueOutputLineProcessor;
 import com.google.idea.blaze.base.model.primitives.Label;
@@ -67,14 +66,15 @@
                     Blaze.getBuildSystemProvider(project).getBinaryPath(), BlazeCommandName.BUILD);
             WorkspaceRoot workspaceRoot = WorkspaceRoot.fromProject(project);
 
+            BlazeApkDeployInfoProtoHelper deployInfoHelper =
+                new BlazeApkDeployInfoProtoHelper(project, buildFlags);
+            BuildResultHelper buildResultHelper = deployInfoHelper.getBuildResultHelper();
+
             command
                 .addTargets(label)
                 .addBlazeFlags("--output_groups=+android_deploy_info")
                 .addBlazeFlags(buildFlags)
-                .addBlazeFlags(BlazeFlags.EXPERIMENTAL_SHOW_ARTIFACTS);
-
-            BlazeApkDeployInfoProtoHelper deployInfoHelper =
-                new BlazeApkDeployInfoProtoHelper(project, buildFlags);
+                .addBlazeFlags(buildResultHelper.getBuildFlags());
 
             SaveUtil.saveAllFiles();
             int retVal =
@@ -82,8 +82,7 @@
                     .addBlazeCommand(command.build())
                     .context(context)
                     .stderr(
-                        LineProcessingOutputStream.of(
-                            deployInfoHelper.getLineProcessor(),
+                        buildResultHelper.stderr(
                             new IssueOutputLineProcessor(project, context, workspaceRoot)))
                     .build()
                     .run();
diff --git a/base/src/META-INF/blaze-base.xml b/base/src/META-INF/blaze-base.xml
index 781e35a..ed49eb9 100644
--- a/base/src/META-INF/blaze-base.xml
+++ b/base/src/META-INF/blaze-base.xml
@@ -326,6 +326,7 @@
     <extensionPoint qualifiedName="com.google.idea.blaze.BlazeTestXmlFinderStrategy" interface="com.google.idea.blaze.base.run.testlogs.BlazeTestXmlFinderStrategy"/>
     <extensionPoint qualifiedName="com.google.idea.blaze.BlazeTestEventsHandler" interface="com.google.idea.blaze.base.run.smrunner.BlazeTestEventsHandler"/>
     <extensionPoint qualifiedName="com.google.idea.blaze.AttributeSpecificStringLiteralReferenceProvider" interface="com.google.idea.blaze.base.lang.buildfile.references.AttributeSpecificStringLiteralReferenceProvider"/>
+    <extensionPoint qualifiedName="com.google.idea.blaze.EventLogger" interface="com.google.idea.blaze.base.logging.EventLogger"/>
   </extensionPoints>
 
   <extensions defaultExtensionNs="com.google.idea.blaze">
diff --git a/base/src/com/google/idea/blaze/base/async/process/LineProcessingOutputStream.java b/base/src/com/google/idea/blaze/base/async/process/LineProcessingOutputStream.java
index 8aa44a8..5357fdf 100644
--- a/base/src/com/google/idea/blaze/base/async/process/LineProcessingOutputStream.java
+++ b/base/src/com/google/idea/blaze/base/async/process/LineProcessingOutputStream.java
@@ -15,11 +15,11 @@
  */
 package com.google.idea.blaze.base.async.process;
 
-import com.google.common.collect.Lists;
+import static java.nio.charset.StandardCharsets.UTF_8;
+
+import com.google.common.collect.ImmutableList;
 import java.io.IOException;
 import java.io.OutputStream;
-import java.util.List;
-import org.jetbrains.annotations.NotNull;
 
 /** An base output stream which marshals output into newline-delimited segments for processing. */
 public final class LineProcessingOutputStream extends OutputStream {
@@ -31,25 +31,29 @@
      *
      * @return Whether line processing should continue
      */
-    boolean processLine(@NotNull String line);
+    boolean processLine(String line);
   }
 
-  @NotNull private final StringBuffer stringBuffer = new StringBuffer();
+  private final StringBuffer stringBuffer = new StringBuffer();
   private volatile boolean closed;
-  @NotNull private final List<LineProcessor> lineProcessors;
+  private final ImmutableList<LineProcessor> lineProcessors;
 
-  LineProcessingOutputStream(@NotNull LineProcessor... lineProcessors) {
-    this.lineProcessors = Lists.newArrayList(lineProcessors);
+  LineProcessingOutputStream(ImmutableList<LineProcessor> lineProcessors) {
+    this.lineProcessors = lineProcessors;
   }
 
-  public static LineProcessingOutputStream of(@NotNull LineProcessor... lineProcessors) {
+  public static LineProcessingOutputStream of(LineProcessor... lineProcessors) {
+    return new LineProcessingOutputStream(ImmutableList.copyOf(lineProcessors));
+  }
+
+  public static LineProcessingOutputStream of(ImmutableList<LineProcessor> lineProcessors) {
     return new LineProcessingOutputStream(lineProcessors);
   }
 
   @Override
   public synchronized void write(byte[] b, int off, int len) {
     if (!closed) {
-      String text = new String(b, off, len);
+      String text = new String(b, off, len, UTF_8);
       stringBuffer.append(text);
 
       while (true) {
diff --git a/base/src/com/google/idea/blaze/base/command/buildresult/BuildResultHelper.java b/base/src/com/google/idea/blaze/base/command/buildresult/BuildResultHelper.java
new file mode 100644
index 0000000..9adf5e2
--- /dev/null
+++ b/base/src/com/google/idea/blaze/base/command/buildresult/BuildResultHelper.java
@@ -0,0 +1,65 @@
+/*
+ * Copyright 2017 The Bazel Authors. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.idea.blaze.base.command.buildresult;
+
+import com.google.common.collect.ImmutableList;
+import com.google.idea.blaze.base.async.process.LineProcessingOutputStream.LineProcessor;
+import com.google.idea.common.experiments.BoolExperiment;
+import java.io.File;
+import java.io.OutputStream;
+import java.util.List;
+import java.util.function.Predicate;
+
+/** Assists in getting build artifacts from a build operation. */
+public interface BuildResultHelper {
+  // This experiment does *not* work yet and should remain off
+  BoolExperiment USE_BEP = new BoolExperiment("use.bep", false);
+
+  /**
+   * Constructs a new build result helper.
+   *
+   * @param files A filter for the output artifacts you are interested in.
+   */
+  static BuildResultHelper forFiles(Predicate<String> files) {
+    return USE_BEP.getValue()
+        ? new BuildResultHelperBep(files)
+        : new BuildResultHelperStderr(files);
+  }
+
+  /**
+   * Returns the build flags necessary for the build result helper to work.
+   *
+   * <p>The user must add these flags to their build command.
+   */
+  List<String> getBuildFlags();
+
+  /**
+   * Returns an output stream to be passed to the external task's stderr.
+   *
+   * <p>The user must pipe blaze's stderr to this output stream.
+   *
+   * @param lineProcessors Any additional line processors you want on stderr output.
+   */
+  OutputStream stderr(LineProcessor... lineProcessors);
+
+  /**
+   * Returns the build result. May only be called once the build is complete, or no artifacts will
+   * be returned.
+   *
+   * @return The build artifacts from the build operation.
+   */
+  ImmutableList<File> getBuildArtifacts();
+}
diff --git a/base/src/com/google/idea/blaze/base/command/buildresult/BuildResultHelperBep.java b/base/src/com/google/idea/blaze/base/command/buildresult/BuildResultHelperBep.java
new file mode 100644
index 0000000..8a32e85
--- /dev/null
+++ b/base/src/com/google/idea/blaze/base/command/buildresult/BuildResultHelperBep.java
@@ -0,0 +1,98 @@
+/*
+ * Copyright 2017 The Bazel Authors. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.idea.blaze.base.command.buildresult;
+
+import com.google.common.collect.ImmutableList;
+import com.google.idea.blaze.base.async.process.LineProcessingOutputStream;
+import com.google.idea.blaze.base.async.process.LineProcessingOutputStream.LineProcessor;
+import com.google.repackaged.devtools.build.lib.buildeventstream.BuildEventStreamProtos.BuildEvent;
+import com.google.repackaged.devtools.build.lib.buildeventstream.BuildEventStreamProtos.BuildEventId;
+import com.google.repackaged.devtools.build.lib.buildeventstream.BuildEventStreamProtos.BuildEventId.IdCase;
+import com.intellij.openapi.diagnostic.Logger;
+import java.io.BufferedInputStream;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.util.List;
+import java.util.UUID;
+import java.util.function.Predicate;
+
+/**
+ * Build event protocol implementation to get build results.
+ *
+ * <p>The build even protocol (BEP for short) is a proto-based protocol used by bazel to communicate
+ * build events.
+ */
+class BuildResultHelperBep implements BuildResultHelper {
+  private static final Logger logger = Logger.getInstance(BuildResultHelperBep.class);
+  private final File outputFile;
+  private final Predicate<String> fileFilter;
+  private ImmutableList<File> result;
+
+  BuildResultHelperBep(Predicate<String> fileFilter) {
+    this.fileFilter = fileFilter;
+    File tempDir = new File(System.getProperty("java.io.tmpdir"));
+    String suffix = UUID.randomUUID().toString();
+    String fileName = "intellij-bep-" + suffix;
+    this.outputFile = new File(tempDir, fileName);
+  }
+
+  @Override
+  public List<String> getBuildFlags() {
+    return ImmutableList.of("--experimental_build_event_binary_file=" + outputFile.getPath());
+  }
+
+  @Override
+  public OutputStream stderr(LineProcessor... lineProcessors) {
+    return LineProcessingOutputStream.of(ImmutableList.copyOf(lineProcessors));
+  }
+
+  @Override
+  public ImmutableList<File> getBuildArtifacts() {
+    if (result == null) {
+      result = readResult();
+    }
+    return result;
+  }
+
+  private ImmutableList<File> readResult() {
+    ImmutableList.Builder<File> result = ImmutableList.builder();
+    try (InputStream inputStream = new BufferedInputStream(new FileInputStream(outputFile))) {
+      BuildEvent buildEvent;
+      while ((buildEvent = BuildEvent.parseDelimitedFrom(inputStream)) != null) {
+        BuildEventId buildEventId = buildEvent.getId();
+        // Note: This doesn't actually work. BEP does not issue these for actions
+        // that don't execute during the build, so we can't find the files
+        // for a no-op build the way we can for --experimental_show_artifacts
+        if (buildEventId.getIdCase() == IdCase.ACTION_COMPLETED) {
+          String output = buildEventId.getActionCompleted().getPrimaryOutput();
+          if (fileFilter.test(output)) {
+            result.add(new File(output));
+          }
+        }
+      }
+    } catch (IOException e) {
+      logger.error(e);
+      return ImmutableList.of();
+    }
+    if (!outputFile.delete()) {
+      logger.warn("Could not delete BEP output file: " + outputFile);
+    }
+    return result.build();
+  }
+}
diff --git a/base/src/com/google/idea/blaze/base/command/buildresult/BuildResultHelperStderr.java b/base/src/com/google/idea/blaze/base/command/buildresult/BuildResultHelperStderr.java
new file mode 100644
index 0000000..ef34dd6
--- /dev/null
+++ b/base/src/com/google/idea/blaze/base/command/buildresult/BuildResultHelperStderr.java
@@ -0,0 +1,58 @@
+/*
+ * Copyright 2017 The Bazel Authors. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.idea.blaze.base.command.buildresult;
+
+import com.google.common.collect.ImmutableList;
+import com.google.idea.blaze.base.async.process.LineProcessingOutputStream;
+import com.google.idea.blaze.base.async.process.LineProcessingOutputStream.LineProcessor;
+import com.google.idea.blaze.base.command.BlazeFlags;
+import java.io.File;
+import java.io.OutputStream;
+import java.util.List;
+import java.util.function.Predicate;
+
+class BuildResultHelperStderr implements BuildResultHelper {
+  private final ImmutableList.Builder<File> buildArtifacts = ImmutableList.builder();
+  private final ExperimentalShowArtifactsLineProcessor experimentalShowArtifactsLineProcessor;
+  private ImmutableList<File> result;
+
+  BuildResultHelperStderr(Predicate<String> fileFilter) {
+    experimentalShowArtifactsLineProcessor =
+        new ExperimentalShowArtifactsLineProcessor(buildArtifacts, fileFilter);
+  }
+
+  @Override
+  public List<String> getBuildFlags() {
+    return ImmutableList.of(BlazeFlags.EXPERIMENTAL_SHOW_ARTIFACTS);
+  }
+
+  @Override
+  public OutputStream stderr(LineProcessor... lineProcessors) {
+    return LineProcessingOutputStream.of(
+        ImmutableList.<LineProcessor>builder()
+            .add(experimentalShowArtifactsLineProcessor)
+            .add(lineProcessors)
+            .build());
+  }
+
+  @Override
+  public ImmutableList<File> getBuildArtifacts() {
+    if (result == null) {
+      result = buildArtifacts.build();
+    }
+    return result;
+  }
+}
diff --git a/base/src/com/google/idea/blaze/base/command/ExperimentalShowArtifactsLineProcessor.java b/base/src/com/google/idea/blaze/base/command/buildresult/ExperimentalShowArtifactsLineProcessor.java
similarity index 78%
rename from base/src/com/google/idea/blaze/base/command/ExperimentalShowArtifactsLineProcessor.java
rename to base/src/com/google/idea/blaze/base/command/buildresult/ExperimentalShowArtifactsLineProcessor.java
index 9698b62..609867c 100644
--- a/base/src/com/google/idea/blaze/base/command/ExperimentalShowArtifactsLineProcessor.java
+++ b/base/src/com/google/idea/blaze/base/command/buildresult/ExperimentalShowArtifactsLineProcessor.java
@@ -13,29 +13,25 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package com.google.idea.blaze.base.command;
+package com.google.idea.blaze.base.command.buildresult;
 
+import com.google.common.collect.ImmutableList;
 import com.google.idea.blaze.base.async.process.LineProcessingOutputStream;
 import java.io.File;
-import java.util.List;
 import java.util.function.Predicate;
 import org.jetbrains.annotations.NotNull;
 
 /** Collects the output of --experimental_show_artifacts */
-public class ExperimentalShowArtifactsLineProcessor
-    implements LineProcessingOutputStream.LineProcessor {
+class ExperimentalShowArtifactsLineProcessor implements LineProcessingOutputStream.LineProcessor {
   private static final String OUTPUT_START = "Build artifacts:";
   private static final String OUTPUT_MARKER = ">>>";
 
-  private final List<File> fileList;
+  private final ImmutableList.Builder<File> fileList;
   private final Predicate<String> filter;
   private boolean afterBuildResult = false;
 
-  public ExperimentalShowArtifactsLineProcessor(List<File> fileList) {
-    this(fileList, (value) -> true);
-  }
-
-  public ExperimentalShowArtifactsLineProcessor(List<File> fileList, Predicate<String> filter) {
+  ExperimentalShowArtifactsLineProcessor(
+      ImmutableList.Builder<File> fileList, Predicate<String> filter) {
     this.fileList = fileList;
     this.filter = filter;
   }
diff --git a/base/src/com/google/idea/blaze/base/issueparser/BlazeIssueParser.java b/base/src/com/google/idea/blaze/base/issueparser/BlazeIssueParser.java
index 2ebd868..df8a28b 100644
--- a/base/src/com/google/idea/blaze/base/issueparser/BlazeIssueParser.java
+++ b/base/src/com/google/idea/blaze/base/issueparser/BlazeIssueParser.java
@@ -36,11 +36,11 @@
 import java.util.regex.Pattern;
 import javax.annotation.Nullable;
 
-
 /** Parses blaze output for compile errors. */
 public class BlazeIssueParser {
 
-  private static class ParseResult {
+  /** Result from parsing the current line */
+  public static class ParseResult {
 
     public static final ParseResult NEEDS_MORE_INPUT = new ParseResult(true, null);
 
@@ -132,7 +132,10 @@
   }
 
   /** Falls back to returning -1 if no integer can be parsed. */
-  public static int parseOptionalInt(String intString) {
+  public static int parseOptionalInt(@Nullable String intString) {
+    if (intString == null) {
+      return -1;
+    }
     try {
       return Integer.parseInt(intString);
     } catch (NumberFormatException e) {
diff --git a/base/src/com/google/idea/blaze/base/lang/buildfile/validation/BuildElementValidation.java b/base/src/com/google/idea/blaze/base/lang/buildfile/validation/BuildElementValidation.java
index 138908c..c069bb9 100644
--- a/base/src/com/google/idea/blaze/base/lang/buildfile/validation/BuildElementValidation.java
+++ b/base/src/com/google/idea/blaze/base/lang/buildfile/validation/BuildElementValidation.java
@@ -15,6 +15,7 @@
  */
 package com.google.idea.blaze.base.lang.buildfile.validation;
 
+import com.google.common.collect.ImmutableList;
 import com.google.idea.blaze.base.lang.buildfile.psi.DictionaryLiteral;
 import com.google.idea.blaze.base.lang.buildfile.psi.FunctionStatement;
 import com.google.idea.blaze.base.lang.buildfile.psi.GlobExpression;
@@ -38,14 +39,21 @@
   private static final EnumSet<Build.Attribute.Discriminator> LIST_TYPES =
       EnumSet.of(
           Discriminator.STRING_LIST,
+          Discriminator.DISTRIBUTION_SET,
           Discriminator.LABEL_LIST,
           Discriminator.OUTPUT_LIST,
           Discriminator.FILESET_ENTRY_LIST,
           Discriminator.INTEGER_LIST,
-          Discriminator.LICENSE);
+          Discriminator.LICENSE,
+          Discriminator.SELECTOR_LIST);
 
   private static final EnumSet<Build.Attribute.Discriminator> DICT_TYPES =
-      EnumSet.of(Discriminator.LABEL_LIST_DICT, Discriminator.STRING_LIST_DICT);
+      EnumSet.of(
+          Discriminator.LABEL_LIST_DICT,
+          Discriminator.STRING_DICT,
+          Discriminator.STRING_LIST_DICT,
+          Discriminator.STRING_DICT_UNARY,
+          Discriminator.LABEL_DICT_UNARY);
 
   private static final EnumSet<Build.Attribute.Discriminator> STRING_TYPES =
       EnumSet.of(
@@ -58,9 +66,20 @@
   private static final EnumSet<Build.Attribute.Discriminator> INTEGER_TYPES =
       EnumSet.of(Discriminator.INTEGER, Discriminator.BOOLEAN, Discriminator.TRISTATE);
 
+  // This enum list is duplicated several times through Bazel source code. In some places there are
+  // additional items not covered here. Don't show spurious errors when more items are added.
+  private static final EnumSet<Build.Attribute.Discriminator> HANDLED_TYPES =
+      EnumSet.copyOf(
+          ImmutableList.<Discriminator>builder()
+              .addAll(LIST_TYPES)
+              .addAll(DICT_TYPES)
+              .addAll(STRING_TYPES)
+              .addAll(INTEGER_TYPES)
+              .build());
+
   /** Returns false iff we know with certainty that the element cannot resolve to the given type. */
   public static boolean possiblyValidType(PsiElement element, Build.Attribute.Discriminator type) {
-    if (type == Discriminator.UNKNOWN) {
+    if (!HANDLED_TYPES.contains(type)) {
       return true;
     }
     if (element instanceof ListLiteral || element instanceof GlobExpression) {
diff --git a/base/src/com/google/idea/blaze/base/logging/EventLogger.java b/base/src/com/google/idea/blaze/base/logging/EventLogger.java
new file mode 100644
index 0000000..5f2a701
--- /dev/null
+++ b/base/src/com/google/idea/blaze/base/logging/EventLogger.java
@@ -0,0 +1,55 @@
+/*
+ * Copyright 2017 The Bazel Authors. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.idea.blaze.base.logging;
+
+import com.intellij.openapi.extensions.ExtensionPointName;
+import java.util.Map;
+import org.jetbrains.annotations.Nullable;
+
+/**
+ * Forwards the event logs to an applicable receiver extension or discard them if no applicable
+ * receivers exist.
+ */
+public interface EventLogger {
+  ExtensionPointName<EventLogger> EP_NAME =
+      new ExtensionPointName<>("com.google.idea.blaze.EventLogger");
+
+  static EventLogger getInstance() {
+    for (EventLogger logger : EP_NAME.getExtensions()) {
+      if (logger.isApplicable()) {
+        return logger;
+      }
+    }
+    return NullEventLogger.SINGLETON;
+  }
+
+  boolean isApplicable();
+
+  default void log(String eventType, Map<String, String> keyValues) {
+    log(eventType, keyValues, null);
+  }
+
+  default void log(
+      String eventType, Map<String, String> keyValues, @Nullable Long timestampInMillis) {
+    log(eventType, keyValues, timestampInMillis, null);
+  }
+
+  void log(
+      String eventType,
+      Map<String, String> keyValues,
+      @Nullable Long timestampInMillis,
+      @Nullable Long durationInNanos);
+}
diff --git a/base/src/com/google/idea/blaze/base/logging/NullEventLogger.java b/base/src/com/google/idea/blaze/base/logging/NullEventLogger.java
new file mode 100644
index 0000000..15562ab
--- /dev/null
+++ b/base/src/com/google/idea/blaze/base/logging/NullEventLogger.java
@@ -0,0 +1,38 @@
+/*
+ * Copyright 2017 The Bazel Authors. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.idea.blaze.base.logging;
+
+import java.util.Map;
+import org.jetbrains.annotations.Nullable;
+
+/** No-op logger used when no logger is not available to receive logs. */
+public class NullEventLogger implements EventLogger {
+  static final NullEventLogger SINGLETON = new NullEventLogger();
+
+  private NullEventLogger() {}
+
+  @Override
+  public boolean isApplicable() {
+    return true;
+  }
+
+  @Override
+  public void log(
+      String eventType,
+      Map<String, String> keyValues,
+      @Nullable Long timestampInMillis,
+      @Nullable Long durationInNanos) {}
+}
diff --git a/base/src/com/google/idea/blaze/base/run/smrunner/BlazeTestEventsHandler.java b/base/src/com/google/idea/blaze/base/run/smrunner/BlazeTestEventsHandler.java
index 7bb4325..3ecdeea 100644
--- a/base/src/com/google/idea/blaze/base/run/smrunner/BlazeTestEventsHandler.java
+++ b/base/src/com/google/idea/blaze/base/run/smrunner/BlazeTestEventsHandler.java
@@ -116,11 +116,11 @@
 
   public String testLocationUrl(
       @Nullable Kind kind, String parentSuite, String name, @Nullable String className) {
-    String base = SmRunnerUtils.GENERIC_TEST_PROTOCOL + URLUtil.SCHEME_SEPARATOR + name;
+    String base = SmRunnerUtils.GENERIC_TEST_PROTOCOL + URLUtil.SCHEME_SEPARATOR;
     if (Strings.isNullOrEmpty(className)) {
-      return base;
+      return base + name;
     }
-    return base + SmRunnerUtils.TEST_NAME_PARTS_SPLITTER + className;
+    return base + className + SmRunnerUtils.TEST_NAME_PARTS_SPLITTER + name;
   }
 
   /** Whether to skip logging a {@link TestSuite}. */
diff --git a/base/src/com/google/idea/blaze/base/settings/ui/ProjectViewUi.java b/base/src/com/google/idea/blaze/base/settings/ui/ProjectViewUi.java
index 5023368..ae6a917 100644
--- a/base/src/com/google/idea/blaze/base/settings/ui/ProjectViewUi.java
+++ b/base/src/com/google/idea/blaze/base/settings/ui/ProjectViewUi.java
@@ -133,7 +133,7 @@
     return editor;
   }
 
-  public void fillUi(JPanel canvas, int indentLevel) {
+  public void fillUi(JPanel canvas) {
     String tooltip =
         "Enter a project view descriptor file."
             + (Blaze.defaultBuildSystem() == BuildSystem.Blaze
@@ -149,9 +149,9 @@
 
     JBLabel labelsLabel = new JBLabel("Project View");
     labelsLabel.setToolTipText(tooltip);
-    canvas.add(labelsLabel, UiUtil.getFillLineConstraints(indentLevel));
+    canvas.add(labelsLabel, UiUtil.getFillLineConstraints(0));
 
-    canvas.add(projectViewEditor.getComponent(), UiUtil.getFillLineConstraints(indentLevel));
+    canvas.add(projectViewEditor.getComponent(), UiUtil.getFillLineConstraints(0));
 
     useShared = new JCheckBox(USE_SHARED_PROJECT_VIEW);
     useShared.addActionListener(
@@ -162,7 +162,7 @@
           }
           updateTextAreasEnabled();
         });
-    canvas.add(useShared, UiUtil.getFillLineConstraints(indentLevel));
+    canvas.add(useShared, UiUtil.getFillLineConstraints(0));
   }
 
   public void init(
@@ -185,10 +185,7 @@
     }
 
     useShared.setSelected(useSharedProjectView);
-
-    if (sharedProjectViewText == null) {
-      useShared.setEnabled(false);
-    }
+    useShared.setEnabled(sharedProjectViewText != null);
 
     setDummyWorkspacePathResolverProvider(this.workspacePathResolver);
     setProjectViewText(projectViewText);
diff --git a/base/src/com/google/idea/blaze/base/sync/aspects/BlazeIdeInterfaceAspectsImpl.java b/base/src/com/google/idea/blaze/base/sync/aspects/BlazeIdeInterfaceAspectsImpl.java
index ac83b63..beb53db 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
@@ -30,7 +30,7 @@
 import com.google.idea.blaze.base.command.BlazeCommand;
 import com.google.idea.blaze.base.command.BlazeCommandName;
 import com.google.idea.blaze.base.command.BlazeFlags;
-import com.google.idea.blaze.base.command.ExperimentalShowArtifactsLineProcessor;
+import com.google.idea.blaze.base.command.buildresult.BuildResultHelper;
 import com.google.idea.blaze.base.filecache.FileDiffer;
 import com.google.idea.blaze.base.ideinfo.TargetIdeInfo;
 import com.google.idea.blaze.base.ideinfo.TargetKey;
@@ -190,36 +190,34 @@
           context.push(
               new TimingScope(String.format("Execute%sCommand", Blaze.buildSystemName(project))));
 
-          List<File> result = Lists.newArrayList();
+          String fileExtension = aspectStrategy.getAspectOutputFileExtension();
+          String gzFileExtension = fileExtension + ".gz";
+          Predicate<String> fileFilter =
+              fileName -> fileName.endsWith(fileExtension) || fileName.endsWith(gzFileExtension);
+          BuildResultHelper buildResultHelper = BuildResultHelper.forFiles(fileFilter);
 
           BlazeCommand.Builder blazeCommandBuilder =
               BlazeCommand.builder(getBinaryPath(project), BlazeCommandName.BUILD);
           blazeCommandBuilder.addTargets(targets);
           blazeCommandBuilder.addBlazeFlags(BlazeFlags.KEEP_GOING);
           blazeCommandBuilder
-              .addBlazeFlags(BlazeFlags.EXPERIMENTAL_SHOW_ARTIFACTS)
+              .addBlazeFlags(buildResultHelper.getBuildFlags())
               .addBlazeFlags(BlazeFlags.buildFlags(project, projectViewSet));
 
           aspectStrategy.modifyIdeInfoCommand(blazeCommandBuilder);
 
-          String fileExtension = aspectStrategy.getAspectOutputFileExtension();
-          String gzFileExtension = fileExtension + ".gz";
-          Predicate<String> fileFilter =
-              fileName -> fileName.endsWith(fileExtension) || fileName.endsWith(gzFileExtension);
-
           int retVal =
               ExternalTask.builder(workspaceRoot)
                   .addBlazeCommand(blazeCommandBuilder.build())
                   .context(context)
                   .stderr(
-                      LineProcessingOutputStream.of(
-                          new ExperimentalShowArtifactsLineProcessor(result, fileFilter),
+                      buildResultHelper.stderr(
                           new IssueOutputLineProcessor(project, context, workspaceRoot)))
                   .build()
                   .run();
 
           BuildResult buildResult = BuildResult.fromExitCode(retVal);
-          return new IdeInfoResult(result, buildResult);
+          return new IdeInfoResult(buildResultHelper.getBuildArtifacts(), buildResult);
         });
   }
 
diff --git a/base/src/com/google/idea/blaze/base/wizard2/BlazeSelectProjectViewOption.java b/base/src/com/google/idea/blaze/base/wizard2/BlazeSelectProjectViewOption.java
index 353ecb4..51229f0 100644
--- a/base/src/com/google/idea/blaze/base/wizard2/BlazeSelectProjectViewOption.java
+++ b/base/src/com/google/idea/blaze/base/wizard2/BlazeSelectProjectViewOption.java
@@ -37,9 +37,9 @@
     return false;
   }
 
-  /** Returns the default project name */
-  default String getDefaultProjectName(String workspaceName) {
-    return workspaceName;
+  /** Returns the directory we're importing from, if applicable. */
+  default String getImportDirectory() {
+    return null;
   }
 
   void commit();
diff --git a/base/src/com/google/idea/blaze/base/wizard2/BlazeSelectWorkspaceOption.java b/base/src/com/google/idea/blaze/base/wizard2/BlazeSelectWorkspaceOption.java
index 11bf115..3c9a601 100644
--- a/base/src/com/google/idea/blaze/base/wizard2/BlazeSelectWorkspaceOption.java
+++ b/base/src/com/google/idea/blaze/base/wizard2/BlazeSelectWorkspaceOption.java
@@ -34,6 +34,9 @@
   /** @return the name of the workspace. Used to generate default project names. */
   String getWorkspaceName();
 
+  /** @return the name of the 'branch', if applicable */
+  String getBranchName();
+
   BuildSystem getBuildSystemForWorkspace();
 
   void commit() throws BlazeProjectCommitException;
diff --git a/base/src/com/google/idea/blaze/base/wizard2/GenerateFromBuildFileSelectProjectViewOption.java b/base/src/com/google/idea/blaze/base/wizard2/GenerateFromBuildFileSelectProjectViewOption.java
index 341d023..6e4e74e 100644
--- a/base/src/com/google/idea/blaze/base/wizard2/GenerateFromBuildFileSelectProjectViewOption.java
+++ b/base/src/com/google/idea/blaze/base/wizard2/GenerateFromBuildFileSelectProjectViewOption.java
@@ -120,9 +120,9 @@
   }
 
   @Override
-  public String getDefaultProjectName(String workspaceName) {
+  public String getImportDirectory() {
     File buildFileParent = new File(getBuildFilePath()).getParentFile();
-    return buildFileParent != null ? buildFileParent.getName() : workspaceName;
+    return buildFileParent != null ? buildFileParent.getName() : null;
   }
 
   @Override
diff --git a/base/src/com/google/idea/blaze/base/wizard2/ImportFromWorkspaceProjectViewOption.java b/base/src/com/google/idea/blaze/base/wizard2/ImportFromWorkspaceProjectViewOption.java
index fe43d1c..14a07e0 100644
--- a/base/src/com/google/idea/blaze/base/wizard2/ImportFromWorkspaceProjectViewOption.java
+++ b/base/src/com/google/idea/blaze/base/wizard2/ImportFromWorkspaceProjectViewOption.java
@@ -109,11 +109,11 @@
   }
 
   @Override
-  public String getDefaultProjectName(String workspaceName) {
+  public String getImportDirectory() {
     File projectViewFile = new File(getProjectViewPath());
     File projectViewDirectory = projectViewFile.getParentFile();
     if (projectViewDirectory == null) {
-      return workspaceName;
+      return null;
     }
     return projectViewDirectory.getName();
   }
diff --git a/base/src/com/google/idea/blaze/base/wizard2/UseExistingBazelWorkspaceOption.java b/base/src/com/google/idea/blaze/base/wizard2/UseExistingBazelWorkspaceOption.java
index 51af079..de9d125 100644
--- a/base/src/com/google/idea/blaze/base/wizard2/UseExistingBazelWorkspaceOption.java
+++ b/base/src/com/google/idea/blaze/base/wizard2/UseExistingBazelWorkspaceOption.java
@@ -117,6 +117,11 @@
   }
 
   @Override
+  public String getBranchName() {
+    return null;
+  }
+
+  @Override
   public BlazeValidationResult validate() {
     if (getDirectory().isEmpty()) {
       return BlazeValidationResult.failure("Please select a workspace");
diff --git a/base/src/com/google/idea/blaze/base/wizard2/ui/BlazeEditProjectViewControl.java b/base/src/com/google/idea/blaze/base/wizard2/ui/BlazeEditProjectViewControl.java
index 70001eb..6c23cf1 100644
--- a/base/src/com/google/idea/blaze/base/wizard2/ui/BlazeEditProjectViewControl.java
+++ b/base/src/com/google/idea/blaze/base/wizard2/ui/BlazeEditProjectViewControl.java
@@ -50,6 +50,7 @@
 import com.google.idea.blaze.base.wizard2.ProjectDataDirectoryValidator;
 import com.google.idea.common.experiments.BoolExperiment;
 import com.intellij.ide.RecentProjectsManager;
+import com.intellij.ide.util.PropertiesComponent;
 import com.intellij.openapi.Disposable;
 import com.intellij.openapi.application.ApplicationNamesInfo;
 import com.intellij.openapi.diagnostic.Logger;
@@ -69,8 +70,10 @@
 import java.util.List;
 import java.util.stream.Collectors;
 import javax.annotation.Nullable;
+import javax.swing.ButtonGroup;
 import javax.swing.JLabel;
 import javax.swing.JPanel;
+import javax.swing.JRadioButton;
 import javax.swing.JTextField;
 import org.jetbrains.annotations.NotNull;
 
@@ -83,6 +86,8 @@
 
   private static final BoolExperiment allowAddprojectViewDefaultValues =
       new BoolExperiment("allow.add.project.view.default.values", true);
+  private static final String LAST_WORKSPACE_MODE_PROPERTY =
+      "blaze.edit.project.view.control.last.workspace.mode";
 
   private final JPanel component;
   private final String buildSystemName;
@@ -90,14 +95,27 @@
 
   private TextFieldWithBrowseButton projectDataDirField;
   private JTextField projectNameField;
+  private JRadioButton workspaceDefaultNameOption;
+  private JRadioButton branchDefaultNameOption;
+  private JRadioButton importDirectoryDefaultNameOption;
   private HashCode paramsHash;
   private WorkspaceRoot workspaceRoot;
   private WorkspacePathResolver workspacePathResolver;
+  private BlazeSelectWorkspaceOption workspaceOption;
+  private BlazeSelectProjectViewOption projectViewOption;
+  private boolean isInitialising;
+  private boolean defaultWorkspaceNameModeExplicitlySet;
+
+  private enum InferDefaultNameMode {
+    FromWorkspace,
+    FromBranch,
+    FromImportDirectory,
+  }
 
   public BlazeEditProjectViewControl(BlazeNewProjectBuilder builder, Disposable parentDisposable) {
     this.projectViewUi = new ProjectViewUi(parentDisposable);
     JPanel component = new JPanelProvidingProject(ProjectViewUi.getProject(), new GridBagLayout());
-    fillUi(component, 0);
+    fillUi(component);
     update(builder);
     UiUtil.fillBottom(component);
     this.component = component;
@@ -108,12 +126,12 @@
     return component;
   }
 
-  private void fillUi(JPanel canvas, int indentLevel) {
+  private void fillUi(JPanel canvas) {
     JLabel projectDataDirLabel = new JBLabel("Project data directory:");
 
     Dimension minSize = ProjectViewUi.getMinimumSize();
-    // Add 120 pixels so we have room for our extra fields
-    minSize.setSize(minSize.width, minSize.height + 120);
+    // Add pixels so we have room for our extra fields
+    minSize.setSize(minSize.width, minSize.height + 180);
     canvas.setMinimumSize(minSize);
     canvas.setPreferredSize(minSize);
 
@@ -131,7 +149,7 @@
     projectDataDirField.setToolTipText(dataDirToolTipText);
     projectDataDirLabel.setToolTipText(dataDirToolTipText);
 
-    canvas.add(projectDataDirLabel, UiUtil.getLabelConstraints(indentLevel));
+    canvas.add(projectDataDirLabel, UiUtil.getLabelConstraints(0));
     canvas.add(projectDataDirField, UiUtil.getFillLineConstraints(0));
 
     JLabel projectNameLabel = new JLabel("Project name:");
@@ -139,17 +157,32 @@
     final String projectNameToolTipText = "Project display name.";
     projectNameField.setToolTipText(projectNameToolTipText);
     projectNameLabel.setToolTipText(projectNameToolTipText);
-    canvas.add(projectNameLabel, UiUtil.getLabelConstraints(indentLevel));
+    canvas.add(projectNameLabel, UiUtil.getLabelConstraints(0));
     canvas.add(projectNameField, UiUtil.getFillLineConstraints(0));
 
-    projectViewUi.fillUi(canvas, indentLevel);
+    JLabel defaultNameLabel = new JLabel("Infer name from:");
+    workspaceDefaultNameOption = new JRadioButton("Workspace");
+    branchDefaultNameOption = new JRadioButton("Branch");
+    importDirectoryDefaultNameOption = new JRadioButton("Import Directory");
+    workspaceDefaultNameOption.addItemListener(e -> inferDefaultNameModeSelectionChanged());
+    branchDefaultNameOption.addItemListener(e -> inferDefaultNameModeSelectionChanged());
+    importDirectoryDefaultNameOption.addItemListener(e -> inferDefaultNameModeSelectionChanged());
+    ButtonGroup buttonGroup = new ButtonGroup();
+    buttonGroup.add(workspaceDefaultNameOption);
+    buttonGroup.add(branchDefaultNameOption);
+    buttonGroup.add(importDirectoryDefaultNameOption);
+    canvas.add(defaultNameLabel, UiUtil.getLabelConstraints(0));
+    canvas.add(workspaceDefaultNameOption, UiUtil.getLabelConstraints(0));
+    canvas.add(branchDefaultNameOption, UiUtil.getLabelConstraints(0));
+    canvas.add(importDirectoryDefaultNameOption, UiUtil.getLabelConstraints(0));
+    canvas.add(new JPanel(), UiUtil.getFillLineConstraints(0));
+
+    projectViewUi.fillUi(canvas);
   }
 
   public void update(BlazeNewProjectBuilder builder) {
-    BlazeSelectWorkspaceOption workspaceOption = builder.getWorkspaceOption();
-    BlazeSelectProjectViewOption projectViewOption = builder.getProjectViewOption();
-    String defaultProjectName =
-        projectViewOption.getDefaultProjectName(workspaceOption.getWorkspaceName());
+    this.workspaceOption = builder.getWorkspaceOption();
+    this.projectViewOption = builder.getProjectViewOption();
     WorkspaceRoot workspaceRoot = workspaceOption.getWorkspaceRoot();
     WorkspacePath workspacePath = projectViewOption.getSharedProjectView();
     String initialProjectViewText = projectViewOption.getInitialProjectViewText();
@@ -161,7 +194,6 @@
     HashCode hashCode =
         Hashing.md5()
             .newHasher()
-            .putUnencodedChars(defaultProjectName)
             .putUnencodedChars(workspaceRoot.toString())
             .putUnencodedChars(workspacePath != null ? workspacePath.toString() : "")
             .putUnencodedChars(initialProjectViewText != null ? initialProjectViewText : "")
@@ -171,13 +203,14 @@
     // If any params have changed, reinit the control
     if (!hashCode.equals(paramsHash)) {
       this.paramsHash = hashCode;
+      this.isInitialising = true;
       init(
-          defaultProjectName,
           workspaceRoot,
           workspacePathResolver,
           workspacePath,
           initialProjectViewText,
           allowAddDefaultValues);
+      this.isInitialising = false;
     }
   }
 
@@ -199,7 +232,6 @@
   }
 
   private void init(
-      String defaultProjectName,
       WorkspaceRoot workspaceRoot,
       WorkspacePathResolver workspacePathResolver,
       @Nullable WorkspacePath sharedProjectView,
@@ -209,12 +241,11 @@
       initialProjectViewText =
           modifyInitialProjectView(initialProjectViewText, workspacePathResolver);
     }
-
     this.workspaceRoot = workspaceRoot;
     this.workspacePathResolver = workspacePathResolver;
-    projectNameField.setText(defaultProjectName);
-    String defaultDataDir = getDefaultProjectDataDirectory(defaultProjectName);
-    projectDataDirField.setText(defaultDataDir);
+
+    updateDefaultProjectNameUiState();
+    updateDefaultProjectName();
 
     String projectViewText = "";
     File sharedProjectViewFile = null;
@@ -246,6 +277,82 @@
         false /* allowEditShared - not allowed during import */);
   }
 
+  private void updateDefaultProjectNameUiState() {
+    workspaceDefaultNameOption.setEnabled(true);
+    branchDefaultNameOption.setEnabled(workspaceOption.getBranchName() != null);
+    importDirectoryDefaultNameOption.setEnabled(projectViewOption.getImportDirectory() != null);
+
+    InferDefaultNameMode inferDefaultNameMode = InferDefaultNameMode.FromImportDirectory;
+    try {
+      String lastModeString =
+          PropertiesComponent.getInstance().getValue(LAST_WORKSPACE_MODE_PROPERTY);
+      if (lastModeString != null) {
+        inferDefaultNameMode = InferDefaultNameMode.valueOf(lastModeString);
+      }
+    } catch (IllegalArgumentException e) {
+      // Ignore
+    }
+    switch (inferDefaultNameMode) {
+      case FromWorkspace:
+        workspaceDefaultNameOption.setSelected(true);
+        break;
+      case FromBranch:
+        if (workspaceOption.getBranchName() != null) {
+          branchDefaultNameOption.setSelected(true);
+        } else {
+          workspaceDefaultNameOption.setSelected(true);
+        }
+        break;
+      case FromImportDirectory:
+        if (projectViewOption.getImportDirectory() != null) {
+          importDirectoryDefaultNameOption.setSelected(true);
+        } else {
+          workspaceDefaultNameOption.setSelected(true);
+        }
+        break;
+      default:
+        throw new AssertionError("Illegal workspace name mode");
+    }
+  }
+
+  private InferDefaultNameMode getInferDefaultNameMode() {
+    if (workspaceDefaultNameOption.isSelected()) {
+      return InferDefaultNameMode.FromWorkspace;
+    } else if (branchDefaultNameOption.isSelected()) {
+      return InferDefaultNameMode.FromBranch;
+    } else if (importDirectoryDefaultNameOption.isSelected()) {
+      return InferDefaultNameMode.FromImportDirectory;
+    }
+    return InferDefaultNameMode.FromWorkspace;
+  }
+
+  private void inferDefaultNameModeSelectionChanged() {
+    if (!isInitialising) {
+      updateDefaultProjectName();
+      this.defaultWorkspaceNameModeExplicitlySet = true;
+    }
+  }
+
+  private void updateDefaultProjectName() {
+    String defaultProjectName = getDefaultName(getInferDefaultNameMode());
+    projectNameField.setText(defaultProjectName);
+    String defaultDataDir = getDefaultProjectDataDirectory(defaultProjectName);
+    projectDataDirField.setText(defaultDataDir);
+  }
+
+  private String getDefaultName(InferDefaultNameMode inferDefaultNameMode) {
+    switch (inferDefaultNameMode) {
+      case FromWorkspace:
+        return workspaceOption.getWorkspaceName();
+      case FromBranch:
+        return workspaceOption.getBranchName();
+      case FromImportDirectory:
+        return projectViewOption.getImportDirectory();
+      default:
+        throw new AssertionError("Invalid workspace name mode.");
+    }
+  }
+
   private static String getDefaultProjectDataDirectory(String projectName) {
     File defaultDataDirectory = new File(getDefaultProjectsDirectory());
     File desiredLocation = new File(defaultDataDirectory, projectName);
@@ -452,4 +559,12 @@
         .setProjectName(projectName)
         .setProjectDataDirectory(projectDataDirectory);
   }
+
+  public void commit() {
+    if (defaultWorkspaceNameModeExplicitlySet) {
+      InferDefaultNameMode inferDefaultNameMode = getInferDefaultNameMode();
+      PropertiesComponent.getInstance()
+          .setValue(LAST_WORKSPACE_MODE_PROPERTY, inferDefaultNameMode.toString());
+    }
+  }
 }
diff --git a/base/tests/integrationtests/com/google/idea/blaze/base/lang/buildfile/validation/BuiltInRuleAnnotatorTest.java b/base/tests/integrationtests/com/google/idea/blaze/base/lang/buildfile/validation/BuiltInRuleAnnotatorTest.java
index 28eece6..2459af6 100644
--- a/base/tests/integrationtests/com/google/idea/blaze/base/lang/buildfile/validation/BuiltInRuleAnnotatorTest.java
+++ b/base/tests/integrationtests/com/google/idea/blaze/base/lang/buildfile/validation/BuiltInRuleAnnotatorTest.java
@@ -55,6 +55,9 @@
   private static final AttributeDefinition NEVERLINK_ATTRIBUTE =
       new AttributeDefinition("neverlink", Discriminator.BOOLEAN, false, null, null);
 
+  private static final AttributeDefinition VALUES_ATTRIBUTE =
+      new AttributeDefinition("values", Discriminator.STRING_DICT, true, null, null);
+
   private static final RuleDefinition JAVA_TEST =
       new RuleDefinition(
           "java_test",
@@ -62,6 +65,12 @@
               "name", NAME_ATTRIBUTE, "srcs", SRCS_ATTRIBUTE, "neverlink", NEVERLINK_ATTRIBUTE),
           null);
 
+  private static final RuleDefinition CONFIG_SETTING =
+      new RuleDefinition(
+          "config_setting",
+          ImmutableMap.of("name", NAME_ATTRIBUTE, "values", VALUES_ATTRIBUTE),
+          null);
+
   private MockBuildLanguageSpecProvider specProvider;
 
   @Before
@@ -95,6 +104,33 @@
   }
 
   @Test
+  public void testNoErrorsForValidStringDict() {
+    specProvider.setRules(ImmutableMap.of(CONFIG_SETTING.name, CONFIG_SETTING));
+    BuildFile file =
+        createBuildFile(
+            new WorkspacePath("java/com/google/BUILD"),
+            "config_setting(",
+            "    name = 'setting',",
+            "    values = {'key1', 'value1', 'key2', 'value2'},",
+            ")");
+    assertNoErrors(file);
+  }
+
+  @Test
+  public void testErrorForInvalidDict() {
+    specProvider.setRules(ImmutableMap.of(CONFIG_SETTING.name, CONFIG_SETTING));
+    BuildFile file =
+        createBuildFile(
+            new WorkspacePath("java/com/google/BUILD"),
+            "config_setting(",
+            "    name = 'setting',",
+            "    values = 1,",
+            ")");
+    assertHasError(
+        file, "Invalid value for attribute 'values'. Expected a value of type 'STRING_DICT'");
+  }
+
+  @Test
   public void testGlobTreatedAsList() {
     specProvider.setRules(ImmutableMap.of(JAVA_TEST.name, JAVA_TEST));
     BuildFile file =
diff --git a/clwb/src/com/google/idea/blaze/clwb/run/BlazeCidrRunConfigurationRunner.java b/clwb/src/com/google/idea/blaze/clwb/run/BlazeCidrRunConfigurationRunner.java
index c1b1f8d..c1008cd 100644
--- a/clwb/src/com/google/idea/blaze/clwb/run/BlazeCidrRunConfigurationRunner.java
+++ b/clwb/src/com/google/idea/blaze/clwb/run/BlazeCidrRunConfigurationRunner.java
@@ -15,16 +15,15 @@
  */
 package com.google.idea.blaze.clwb.run;
 
+import com.google.common.collect.ImmutableList;
 import com.google.common.collect.Iterables;
-import com.google.common.collect.Lists;
 import com.google.common.util.concurrent.ListenableFuture;
 import com.google.idea.blaze.base.async.executor.BlazeExecutor;
 import com.google.idea.blaze.base.async.process.ExternalTask;
-import com.google.idea.blaze.base.async.process.LineProcessingOutputStream;
 import com.google.idea.blaze.base.command.BlazeCommand;
 import com.google.idea.blaze.base.command.BlazeCommandName;
 import com.google.idea.blaze.base.command.BlazeFlags;
-import com.google.idea.blaze.base.command.ExperimentalShowArtifactsLineProcessor;
+import com.google.idea.blaze.base.command.buildresult.BuildResultHelper;
 import com.google.idea.blaze.base.issueparser.IssueOutputLineProcessor;
 import com.google.idea.blaze.base.model.primitives.WorkspaceRoot;
 import com.google.idea.blaze.base.projectview.ProjectViewManager;
@@ -50,7 +49,6 @@
 import com.intellij.openapi.vfs.LocalFileSystem;
 import com.jetbrains.cidr.execution.CidrCommandLineState;
 import java.io.File;
-import java.util.List;
 
 /** CLion-specific handler for {@link BlazeCommandRunConfiguration}s. */
 public class BlazeCidrRunConfigurationRunner implements BlazeCommandRunConfigurationRunner {
@@ -109,7 +107,8 @@
     final ProjectViewSet projectViewSet =
         ProjectViewManager.getInstance(project).getProjectViewSet();
 
-    final List<File> outputArtifacts = Lists.newArrayList();
+    BuildResultHelper buildResultHelper = BuildResultHelper.forFiles(file -> true);
+
     final ListenableFuture<Void> buildOperation =
         BlazeExecutor.submitTask(
             project,
@@ -128,9 +127,8 @@
                             BlazeCommandName.BUILD)
                         .addTargets(configuration.getTarget())
                         .addBlazeFlags(BlazeFlags.buildFlags(project, projectViewSet))
-                        .addBlazeFlags(handlerState.getBlazeFlags());
-
-                command.addBlazeFlags("--experimental_show_artifacts");
+                        .addBlazeFlags(handlerState.getBlazeFlags())
+                        .addBlazeFlags(buildResultHelper.getBuildFlags());
 
                 // If we are trying to debug, make sure we are building in debug mode.
                 // This can cause a rebuild, so it is a heavyweight setting.
@@ -142,8 +140,7 @@
                     .addBlazeCommand(command.build())
                     .context(context)
                     .stderr(
-                        LineProcessingOutputStream.of(
-                            new ExperimentalShowArtifactsLineProcessor(outputArtifacts),
+                        buildResultHelper.stderr(
                             new IssueOutputLineProcessor(project, context, workspaceRoot)))
                     .build()
                     .run();
@@ -156,6 +153,7 @@
     } catch (InterruptedException | java.util.concurrent.ExecutionException e) {
       throw new ExecutionException(e);
     }
+    ImmutableList<File> outputArtifacts = buildResultHelper.getBuildArtifacts();
     if (outputArtifacts.isEmpty()) {
       throw new ExecutionException(
           String.format("No output artifacts found when building %s", configuration.getTarget()));
diff --git a/clwb/src/com/google/idea/blaze/clwb/run/producers/BlazeCidrTestConfigurationProducer.java b/clwb/src/com/google/idea/blaze/clwb/run/producers/BlazeCidrTestConfigurationProducer.java
index 0a48b17..b901eb0 100644
--- a/clwb/src/com/google/idea/blaze/clwb/run/producers/BlazeCidrTestConfigurationProducer.java
+++ b/clwb/src/com/google/idea/blaze/clwb/run/producers/BlazeCidrTestConfigurationProducer.java
@@ -17,12 +17,15 @@
 
 import com.google.common.collect.ImmutableList;
 import com.google.idea.blaze.base.command.BlazeCommandName;
+import com.google.idea.blaze.base.model.primitives.Label;
 import com.google.idea.blaze.base.run.BlazeCommandRunConfiguration;
 import com.google.idea.blaze.base.run.BlazeCommandRunConfigurationType;
+import com.google.idea.blaze.base.run.TestTargetHeuristic;
 import com.google.idea.blaze.base.run.producers.BlazeRunConfigurationProducer;
 import com.google.idea.blaze.base.run.state.BlazeCommandRunConfigurationCommonState;
 import com.google.idea.blaze.base.settings.Blaze;
-import com.google.idea.blaze.clwb.run.test.BlazeCidrTestTarget;
+import com.google.idea.blaze.clwb.run.test.GoogleTestLocation;
+import com.google.idea.blaze.clwb.run.test.GoogleTestSpecification;
 import com.intellij.execution.Location;
 import com.intellij.execution.actions.ConfigurationContext;
 import com.intellij.openapi.actionSystem.LangDataKeys;
@@ -60,12 +63,16 @@
     if (element == null) {
       return false;
     }
-    BlazeCidrTestTarget testObject = BlazeCidrTestTarget.findTestObject(element);
-    if (testObject == null) {
+    GoogleTestLocation test = GoogleTestLocation.findGoogleTest(element);
+    if (test == null) {
       return false;
     }
-    sourceElement.set(testObject.element);
-    configuration.setTarget(testObject.label);
+    Label label = getTestTarget(test.getPsiElement());
+    if (label == null) {
+      return false;
+    }
+    sourceElement.set(test.getPsiElement());
+    configuration.setTarget(label);
     BlazeCommandRunConfigurationCommonState handlerState =
         configuration.getHandlerStateIfType(BlazeCommandRunConfigurationCommonState.class);
     if (handlerState == null) {
@@ -74,7 +81,7 @@
     handlerState.setCommand(BlazeCommandName.TEST);
 
     ImmutableList.Builder<String> flags = ImmutableList.builder();
-    String testFilter = testObject.getTestFilterFlag();
+    String testFilter = test.getTestFilterFlag();
     if (testFilter != null) {
       flags.add(testFilter);
     }
@@ -83,7 +90,8 @@
     handlerState.setBlazeFlags(flags.build());
     configuration.setName(
         String.format(
-            "%s test: %s", Blaze.buildSystemName(configuration.getProject()), testObject.name));
+            "%s test: %s",
+            Blaze.buildSystemName(configuration.getProject()), getTestName(label, test.gtest)));
     return true;
   }
 
@@ -102,11 +110,27 @@
     if (element == null) {
       return false;
     }
-    BlazeCidrTestTarget testObject = BlazeCidrTestTarget.findTestObject(element);
-    if (testObject == null) {
+    GoogleTestLocation test = GoogleTestLocation.findGoogleTest(element);
+    if (test == null) {
       return false;
     }
-    return testObject.label.equals(configuration.getTarget())
-        && Objects.equals(handlerState.getTestFilterFlag(), testObject.getTestFilterFlag());
+    Label label = getTestTarget(test.getPsiElement());
+    if (label == null) {
+      return false;
+    }
+    return label.equals(configuration.getTarget())
+        && Objects.equals(handlerState.getTestFilterFlag(), test.getTestFilterFlag());
+  }
+
+  @Nullable
+  private static Label getTestTarget(PsiElement element) {
+    return TestTargetHeuristic.testTargetForPsiElement(element);
+  }
+
+  private static String getTestName(Label target, GoogleTestSpecification gtest) {
+    String filterDescription = gtest.description();
+    return filterDescription != null
+        ? String.format("%s (%s)", filterDescription, target.toString())
+        : target.toString();
   }
 }
diff --git a/clwb/src/com/google/idea/blaze/clwb/run/test/BlazeCidrTestEventsHandler.java b/clwb/src/com/google/idea/blaze/clwb/run/test/BlazeCidrTestEventsHandler.java
index 831a81b..f117615 100644
--- a/clwb/src/com/google/idea/blaze/clwb/run/test/BlazeCidrTestEventsHandler.java
+++ b/clwb/src/com/google/idea/blaze/clwb/run/test/BlazeCidrTestEventsHandler.java
@@ -22,8 +22,6 @@
 import com.intellij.execution.Location;
 import com.intellij.execution.testframework.sm.runner.SMTestLocator;
 import com.intellij.openapi.project.Project;
-import com.intellij.util.io.URLUtil;
-import com.jetbrains.cidr.execution.testing.OCGoogleTestLocationProvider;
 import java.util.ArrayList;
 import java.util.EnumSet;
 import java.util.List;
@@ -39,7 +37,7 @@
 
   @Override
   public SMTestLocator getTestLocator() {
-    return OCGoogleTestLocationProvider.INSTANCE;
+    return BlazeCppTestLocator.INSTANCE;
   }
 
   @Nullable
@@ -47,9 +45,9 @@
   public String getTestFilter(Project project, List<Location<?>> testLocations) {
     List<String> filters = new ArrayList<>();
     for (Location<?> location : testLocations) {
-      BlazeCidrTestTarget target = BlazeCidrTestTarget.findTestObject(location.getPsiElement());
-      if (target != null && target.testFilter != null) {
-        filters.add(target.testFilter);
+      GoogleTestLocation test = GoogleTestLocation.findGoogleTest(location);
+      if (test != null && test.testFilter != null) {
+        filters.add(test.testFilter);
       }
     }
     if (filters.isEmpty()) {
@@ -57,22 +55,4 @@
     }
     return String.format("%s=%s", BlazeFlags.TEST_FILTER, Joiner.on(':').join(filters));
   }
-
-  @Override
-  public String suiteLocationUrl(@Nullable Kind kind, String name) {
-    return OCGoogleTestLocationProvider.PROTOCOL + URLUtil.SCHEME_SEPARATOR + name;
-  }
-
-  @Override
-  public String testLocationUrl(
-      @Nullable Kind kind, String parentSuite, String name, @Nullable String className) {
-    if (className == null) {
-      return OCGoogleTestLocationProvider.PROTOCOL + URLUtil.SCHEME_SEPARATOR + name;
-    }
-    return OCGoogleTestLocationProvider.PROTOCOL
-        + URLUtil.SCHEME_SEPARATOR
-        + className
-        + "."
-        + name;
-  }
 }
diff --git a/clwb/src/com/google/idea/blaze/clwb/run/test/BlazeCppTestLocator.java b/clwb/src/com/google/idea/blaze/clwb/run/test/BlazeCppTestLocator.java
new file mode 100644
index 0000000..283e78d
--- /dev/null
+++ b/clwb/src/com/google/idea/blaze/clwb/run/test/BlazeCppTestLocator.java
@@ -0,0 +1,134 @@
+/*
+ * Copyright 2017 The Bazel Authors. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.idea.blaze.clwb.run.test;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Iterables;
+import com.google.idea.blaze.base.run.smrunner.SmRunnerUtils;
+import com.intellij.execution.Location;
+import com.intellij.execution.testframework.sm.runner.SMTestLocator;
+import com.intellij.openapi.project.Project;
+import com.intellij.psi.PsiElement;
+import com.intellij.psi.search.GlobalSearchScope;
+import com.intellij.util.CommonProcessors;
+import com.jetbrains.cidr.execution.testing.CidrTestUtil;
+import com.jetbrains.cidr.lang.psi.OCMacroCall;
+import com.jetbrains.cidr.lang.psi.OCStruct;
+import com.jetbrains.cidr.lang.symbols.OCSymbol;
+import com.jetbrains.cidr.lang.symbols.cpp.OCStructSymbol;
+import com.jetbrains.cidr.lang.symbols.symtable.OCGlobalProjectSymbolsCache;
+import java.util.Collection;
+import java.util.List;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+import javax.annotation.Nullable;
+
+/** Locate cpp test classes / methods for test UI navigation. */
+public class BlazeCppTestLocator implements SMTestLocator {
+
+  public static final BlazeCppTestLocator INSTANCE = new BlazeCppTestLocator();
+
+  private static final Pattern SUITE_PATTERN = Pattern.compile("((\\w+)/)?(\\w+)(/(\\d+))?");
+  private static final Pattern METHOD_PATTERN = Pattern.compile("(\\w+)(/(\\d+))?");
+
+  private BlazeCppTestLocator() {}
+
+  @Override
+  public List<Location> getLocation(
+      String protocol, String path, Project project, GlobalSearchScope scope) {
+    GoogleTestLocation location = null;
+    if (SmRunnerUtils.GENERIC_SUITE_PROTOCOL.equals(protocol)) {
+      location = getLocation(project, path, null);
+    } else if (SmRunnerUtils.GENERIC_TEST_PROTOCOL.equals(protocol)) {
+      String[] components = path.split(SmRunnerUtils.TEST_NAME_PARTS_SPLITTER);
+      location = components.length != 2 ? null : getLocation(project, components[0], components[1]);
+    }
+    return location != null ? ImmutableList.of(location) : ImmutableList.of();
+  }
+
+  @Nullable
+  private static GoogleTestLocation getLocation(
+      Project project, String suiteComponent, @Nullable String methodComponent) {
+    Matcher matcher = SUITE_PATTERN.matcher(suiteComponent);
+    if (!matcher.matches()) {
+      return null;
+    }
+    String instantiation = matcher.group(2);
+    String suite = matcher.group(3);
+    String method = null;
+    if (methodComponent != null) {
+      matcher = METHOD_PATTERN.matcher(methodComponent);
+      if (!matcher.matches()) {
+        return null;
+      }
+      method = matcher.group(1);
+    }
+    PsiElement psi = findPsiElement(project, instantiation, suite, method);
+    if (psi == null) {
+      return null;
+    }
+    GoogleTestSpecification gtest =
+        new GoogleTestSpecification.FromGtestOutput(suiteComponent, methodComponent);
+    return new GoogleTestLocation(psi, gtest);
+  }
+
+  @Nullable
+  private static PsiElement findPsiElement(
+      Project project,
+      @Nullable String instantiation,
+      @Nullable String suite,
+      @Nullable String method) {
+    if (suite == null) {
+      return null;
+    }
+    OCSymbol<?> symbol;
+    if (method != null) {
+      symbol = CidrTestUtil.findGoogleTestSymbol(project, suite, method);
+    } else if (instantiation != null) {
+      symbol = CidrTestUtil.findGoogleTestInstantiationSymbol(project, suite, instantiation);
+    } else {
+      symbol = findSuiteSymbol(project, suite);
+    }
+    if (symbol == null) {
+      return null;
+    }
+    PsiElement psi = symbol.locateDefinition();
+    while (!(psi instanceof OCStruct || psi instanceof OCMacroCall) && psi != null) {
+      PsiElement prev = psi.getPrevSibling();
+      psi = prev == null ? psi.getParent() : prev;
+    }
+    return psi;
+  }
+
+  @Nullable
+  private static OCStructSymbol findSuiteSymbol(Project project, String suite) {
+    CommonProcessors.FindProcessor<OCSymbol> processor =
+        new CommonProcessors.FindProcessor<OCSymbol>() {
+          @Override
+          protected boolean accept(OCSymbol symbol) {
+            return symbol instanceof OCStructSymbol
+                && CidrTestUtil.isGoogleTestClass((OCStructSymbol) symbol);
+          }
+        };
+    OCGlobalProjectSymbolsCache.processTopLevelAndMemberSymbols(project, processor, suite);
+    if (processor.isFound()) {
+      return (OCStructSymbol) processor.getFoundValue();
+    }
+    Collection<OCStructSymbol> symbolsForSuite =
+        CidrTestUtil.findGoogleTestSymbolsForSuiteSorted(project, suite);
+    return Iterables.getFirst(symbolsForSuite, null);
+  }
+}
diff --git a/clwb/src/com/google/idea/blaze/clwb/run/test/BlazeCidrTestTarget.java b/clwb/src/com/google/idea/blaze/clwb/run/test/GoogleTestLocation.java
similarity index 76%
rename from clwb/src/com/google/idea/blaze/clwb/run/test/BlazeCidrTestTarget.java
rename to clwb/src/com/google/idea/blaze/clwb/run/test/GoogleTestLocation.java
index acf3966..ebf41c1 100644
--- a/clwb/src/com/google/idea/blaze/clwb/run/test/BlazeCidrTestTarget.java
+++ b/clwb/src/com/google/idea/blaze/clwb/run/test/GoogleTestLocation.java
@@ -16,8 +16,8 @@
 package com.google.idea.blaze.clwb.run.test;
 
 import com.google.idea.blaze.base.command.BlazeFlags;
-import com.google.idea.blaze.base.model.primitives.Label;
-import com.google.idea.blaze.base.run.TestTargetHeuristic;
+import com.intellij.execution.Location;
+import com.intellij.execution.PsiLocation;
 import com.intellij.openapi.util.Couple;
 import com.intellij.psi.PsiElement;
 import com.intellij.psi.util.PsiTreeUtil;
@@ -35,23 +35,16 @@
 import java.util.List;
 import javax.annotation.Nullable;
 
-/** A blaze cpp test target, together with optional test filter. */
-public class BlazeCidrTestTarget {
+/** A {@link PsiLocation} with corresponding gtest specification */
+public class GoogleTestLocation extends PsiLocation<PsiElement> {
 
-  public final PsiElement element;
-  public final Label label;
+  public final GoogleTestSpecification gtest;
   @Nullable public final String testFilter;
-  public final String name;
 
-  private BlazeCidrTestTarget(PsiElement element, Label label, @Nullable String testFilter) {
-    this.element = element;
-    this.label = label;
-    this.testFilter = testFilter;
-    if (testFilter != null) {
-      name = String.format("%s (%s)", testFilter, label.toString());
-    } else {
-      name = label.toString();
-    }
+  GoogleTestLocation(PsiElement psi, GoogleTestSpecification gtest) {
+    super(psi);
+    this.gtest = gtest;
+    this.testFilter = gtest.testFilter();
   }
 
   /** The raw test filter string with '--test_filter=' prepended, or null if there is no filter. */
@@ -61,35 +54,15 @@
   }
 
   @Nullable
-  private static BlazeCidrTestTarget createFromFile(@Nullable PsiElement element) {
-    return createFromClassAndMethod(element, null, null);
-  }
-
-  @Nullable
-  private static BlazeCidrTestTarget createFromClass(
-      @Nullable PsiElement element, String className) {
-    return createFromClassAndMethod(element, className, null);
-  }
-
-  @Nullable
-  private static BlazeCidrTestTarget createFromClassAndMethod(
-      @Nullable PsiElement element, String classOrSuiteName, @Nullable String testName) {
-    Label label = TestTargetHeuristic.testTargetForPsiElement(element);
-    if (label == null) {
-      return null;
+  public static GoogleTestLocation findGoogleTest(Location<?> location) {
+    if (location instanceof GoogleTestLocation) {
+      return (GoogleTestLocation) location;
     }
-    String filter = null;
-    if (classOrSuiteName != null) {
-      filter = classOrSuiteName;
-      if (testName != null) {
-        filter += "." + testName;
-      }
-    }
-    return new BlazeCidrTestTarget(element, label, filter);
+    return findGoogleTest(location.getPsiElement());
   }
 
   @Nullable
-  public static BlazeCidrTestTarget findTestObject(PsiElement element) {
+  public static GoogleTestLocation findGoogleTest(PsiElement element) {
     // Copied from on CidrGoogleTestRunConfigurationProducer::findTestObject.
     // Precedence order (decreasing): class/function, macro, file
     PsiElement parent =
@@ -118,8 +91,7 @@
             if (name != null) {
               return createFromClassAndMethod(struct, name.first, name.second);
             }
-            return createFromClass(
-                struct, ((OCStructSymbol) owner).getQualifiedName().getName());
+            return createFromClass(struct, ((OCStructSymbol) owner).getQualifiedName().getName());
           }
         }
       }
@@ -146,8 +118,7 @@
               CidrTestUtil.findGoogleTestSymbol(element.getProject(), suiteName, testName);
           if (symbol != null) {
             OCStruct targetElement = (OCStruct) symbol.locateDefinition();
-            return createFromClassAndMethod(
-                targetElement, suiteName, isSuite ? null : testName);
+            return createFromClassAndMethod(targetElement, suiteName, isSuite ? null : testName);
           }
         }
       }
@@ -158,7 +129,9 @@
                 element.getProject(), suite.first, true);
         if (res.size() != 0) {
           OCStruct struct = (OCStruct) res.iterator().next().locateDefinition();
-          return createFromClassAndMethod(struct, suite.first, null);
+          GoogleTestSpecification gtest =
+              new GoogleTestSpecification.FromPsiElement(suite.first, null, suite.second, null);
+          return new GoogleTestLocation(struct, gtest);
         }
       }
     } else if (parent instanceof OCFile) {
@@ -175,4 +148,26 @@
     }
     return false;
   }
+
+  @Nullable
+  private static GoogleTestLocation createFromFile(@Nullable PsiElement element) {
+    return createFromClassAndMethod(element, null, null);
+  }
+
+  @Nullable
+  private static GoogleTestLocation createFromClass(
+      @Nullable PsiElement element, @Nullable String className) {
+    return createFromClassAndMethod(element, className, null);
+  }
+
+  @Nullable
+  private static GoogleTestLocation createFromClassAndMethod(
+      @Nullable PsiElement element, @Nullable String classOrSuiteName, @Nullable String testName) {
+    if (element == null) {
+      return null;
+    }
+    GoogleTestSpecification gtest =
+        new GoogleTestSpecification.FromPsiElement(classOrSuiteName, testName, null, null);
+    return new GoogleTestLocation(element, gtest);
+  }
 }
diff --git a/clwb/src/com/google/idea/blaze/clwb/run/test/GoogleTestSpecification.java b/clwb/src/com/google/idea/blaze/clwb/run/test/GoogleTestSpecification.java
new file mode 100644
index 0000000..2111410
--- /dev/null
+++ b/clwb/src/com/google/idea/blaze/clwb/run/test/GoogleTestSpecification.java
@@ -0,0 +1,122 @@
+/*
+ * Copyright 2017 The Bazel Authors. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.idea.blaze.clwb.run.test;
+
+import com.google.common.base.Joiner;
+import com.intellij.openapi.util.text.StringUtil;
+import javax.annotation.Nullable;
+
+/** A single gtest test case specification (https://github.com/google/googletest). */
+public interface GoogleTestSpecification {
+
+  /** The gtest filter string. Returns null if there is no filtering. */
+  @Nullable
+  String testFilter();
+
+  /** A human-readable description for this test. Returns null if there is no filtering. */
+  @Nullable
+  String description();
+
+  /**
+   * Built from the raw gtest output, without separating parameter components, etc.<br>
+   * This means there is no ambiguity -- guaranteed to be exactly what the gtest runner expects for
+   * this test case.
+   */
+  class FromGtestOutput implements GoogleTestSpecification {
+
+    private final String suiteComponent;
+    @Nullable private final String methodComponent;
+
+    public FromGtestOutput(String suiteComponent, @Nullable String methodComponent) {
+      this.suiteComponent = suiteComponent;
+      this.methodComponent = methodComponent;
+    }
+
+    @Override
+    public String testFilter() {
+      String method = methodComponent != null ? methodComponent : "*";
+      return String.format("%s.%s", suiteComponent, method);
+    }
+
+    @Override
+    public String description() {
+      return methodComponent == null
+          ? suiteComponent
+          : String.format("%s.%s", suiteComponent, methodComponent);
+    }
+  }
+
+  /**
+   * We don't know whether it's parameterized / typed in this context, so need to provide a more
+   * flexible filter.
+   */
+  class FromPsiElement implements GoogleTestSpecification {
+    @Nullable private final String suiteOrClass;
+    @Nullable private final String method;
+    @Nullable private final String instantiation;
+    @Nullable private final String param;
+
+    public FromPsiElement(
+        @Nullable String suiteOrClass,
+        @Nullable String method,
+        @Nullable String instantiation,
+        @Nullable String param) {
+      this.suiteOrClass = suiteOrClass;
+      this.method = method;
+      this.instantiation = instantiation;
+      this.param = param;
+    }
+
+    @Override
+    @Nullable
+    public String testFilter() {
+      if (suiteOrClass == null) {
+        return null;
+      }
+      String method = StringUtil.notNullize(this.method, "*");
+      String param = StringUtil.notNullize(this.param, "*");
+      if (instantiation != null) {
+        return Joiner.on(':')
+            .join(
+                String.format("%s/%s.%s/%s", instantiation, suiteOrClass, method, param),
+                String.format("%s/%s/%s.%s", instantiation, suiteOrClass, param, method));
+      }
+      // we don't know whether it's parameterized and/or typed, so need to handle all cases
+      return Joiner.on(':')
+          .join(
+              String.format("%s.%s", suiteOrClass, method),
+              String.format("%s/%s.%s", suiteOrClass, param, method),
+              String.format("*/%s.%s/*", suiteOrClass, method),
+              String.format("*/%s/*.%s", suiteOrClass, method));
+    }
+
+    @Override
+    @Nullable
+    public String description() {
+      if (suiteOrClass == null) {
+        return null;
+      }
+      if (method == null) {
+        return suiteOrClass;
+      }
+      if (instantiation == null) {
+        return suiteOrClass + "." + method;
+      }
+      String param = StringUtil.notNullize(this.param, "*");
+      return String.format("%s/%s.%s/%s", instantiation, suiteOrClass, method, param);
+    }
+  }
+}
diff --git a/cpp/src/com/google/idea/blaze/cpp/BlazeConfigurationResolver.java b/cpp/src/com/google/idea/blaze/cpp/BlazeConfigurationResolver.java
index f4b5cd6..2fea837 100644
--- a/cpp/src/com/google/idea/blaze/cpp/BlazeConfigurationResolver.java
+++ b/cpp/src/com/google/idea/blaze/cpp/BlazeConfigurationResolver.java
@@ -54,6 +54,7 @@
 import java.io.File;
 import java.io.IOException;
 import java.io.PrintWriter;
+import java.util.Comparator;
 import java.util.List;
 import java.util.Map;
 import java.util.Set;
@@ -371,7 +372,7 @@
     // If a source file is in two different targets,
     // we can't possibly show how it will be interpreted in both contexts at the same time
     // in the IDE, so just pick the first target after we sort.
-    targetsForSourceFile.sort((o1, o2) -> o1.toString().compareTo(o2.toString()));
+    targetsForSourceFile.sort(Comparator.comparing(TargetKey::toString));
     TargetKey targetKey = Iterables.getFirst(targetsForSourceFile, null);
     assert (targetKey != null);
 
diff --git a/intellij_platform_sdk/build_defs.bzl b/intellij_platform_sdk/build_defs.bzl
index f833101..357308a 100644
--- a/intellij_platform_sdk/build_defs.bzl
+++ b/intellij_platform_sdk/build_defs.bzl
@@ -4,8 +4,8 @@
 INDIRECT_IJ_PRODUCTS = {
     "intellij-latest": "intellij-2016.3.1",
     "android-studio-latest": "android-studio-145.1617.8",
-    "android-studio-beta": "android-studio-2.3.0.4",
-    "clion-latest": "clion-162.1967.7",
+    "android-studio-beta": "android-studio-2.3.0.6",
+    "clion-latest": "clion-2016.3.2",
 }
 
 DIRECT_IJ_PRODUCTS = {
@@ -21,10 +21,6 @@
         ide="android-studio",
         directory="AI_145_1617_8",
     ),
-    "android-studio-2.3.0.3": struct(
-        ide="android-studio",
-        directory="android_studio_2_3_0_3",
-    ),
     "android-studio-2.3.0.4": struct(
         ide="android-studio",
         directory="android_studio_2_3_0_4",
diff --git a/java/src/com/google/idea/blaze/java/wizard2/BlazeEditProjectViewImportWizardStep.java b/java/src/com/google/idea/blaze/java/wizard2/BlazeEditProjectViewImportWizardStep.java
index 4a6bc6a..3d57300 100644
--- a/java/src/com/google/idea/blaze/java/wizard2/BlazeEditProjectViewImportWizardStep.java
+++ b/java/src/com/google/idea/blaze/java/wizard2/BlazeEditProjectViewImportWizardStep.java
@@ -83,6 +83,7 @@
   public void onWizardFinished() throws CommitStepException {
     try {
       getProjectBuilder().commit();
+      control.commit();
     } catch (BlazeProjectCommitException e) {
       throw new CommitStepException(e.getMessage());
     }
diff --git a/proto/proto_deps.jar b/proto/proto_deps.jar
index ac441be..b2e5390 100755
--- a/proto/proto_deps.jar
+++ b/proto/proto_deps.jar
Binary files differ
diff --git a/version.bzl b/version.bzl
index f56966e..5e34b4b 100644
--- a/version.bzl
+++ b/version.bzl
@@ -1,3 +1,3 @@
 """Version of the blaze plugin."""
 
-VERSION = "2017.02.13.1"
+VERSION = "2017.02.27.1"