Import of bazel plugin using copybara

PiperOrigin-RevId: 142767644
diff --git a/BUILD b/BUILD
index 1f6ee16..f87b1dc 100644
--- a/BUILD
+++ b/BUILD
@@ -10,6 +10,7 @@
     tests = [
         "//base:integration_tests",
         "//base:unit_tests",
+        "//ijwb:integration_tests",
         "//ijwb:unit_tests",
         "//java:integration_tests",
         "//java:unit_tests",
diff --git a/WORKSPACE b/WORKSPACE
index c29d33a..edb340b 100644
--- a/WORKSPACE
+++ b/WORKSPACE
@@ -1,17 +1,19 @@
 workspace(name = "intellij_with_bazel")
 
-# The plugin api for IntelliJ 2016.2.5. This is required to build IJwB,
+# Long-lived download links available at: https://www.jetbrains.com/intellij-repository/releases
+
+# The plugin api for IntelliJ 2016.2.4. This is required to build IJwB,
 # and run integration tests.
 new_http_archive(
-    name = "intellij_latest",
+    name = "IC_162_2032_8",
     build_file = "intellij_platform_sdk/BUILD.idea",
-    url = "https://download.jetbrains.com/idea/ideaIC-2016.2.5.tar.gz",
+    url = "https://www.jetbrains.com/intellij-repository/releases/com/jetbrains/intellij/idea/ideaIC/2016.2.4/ideaIC-2016.2.4.zip",
 )
 
 # The plugin api for CLion 2016.2.2. This is required to build CLwB,
 # and run integration tests.
 new_http_archive(
-    name = "clion_latest",
+    name = "CL_162_1967_7",
     build_file = "intellij_platform_sdk/BUILD.clion",
     url = "https://download.jetbrains.com/cpp/CLion-2016.2.2.tar.gz",
 )
@@ -19,7 +21,7 @@
 # The plugin api for Android Studio 2.2 stable. This is required to build ASwB,
 # and run integration tests.
 new_http_archive(
-    name = "android_studio_latest",
+    name = "AI_145_1617_8",
     build_file = "intellij_platform_sdk/BUILD.android_studio",
     url = "https://dl.google.com/dl/android/studio/ide-zips/2.2.0.12/android-studio-ide-145.3276617-linux.zip",
 )
diff --git a/aswb/src/META-INF/aswb.xml b/aswb/src/META-INF/aswb.xml
index ab228a2..ca22494 100644
--- a/aswb/src/META-INF/aswb.xml
+++ b/aswb/src/META-INF/aswb.xml
@@ -36,7 +36,6 @@
     <programRunner implementation="com.google.idea.blaze.android.run.binary.BlazeAndroidBinaryProgramRunner" order="first"/>
     <executor implementation="com.google.idea.blaze.android.run.binary.mobileinstall.IncrementalInstallRunExecutor" order="last"/>
     <executor implementation="com.google.idea.blaze.android.run.binary.mobileinstall.IncrementalInstallDebugExecutor" order="last"/>
-    <applicationService serviceImplementation="com.google.idea.blaze.android.settings.AswbGlobalSettings"/>
     <applicationService serviceInterface="com.google.idea.blaze.base.plugin.BlazePluginId"
                         serviceImplementation="com.google.idea.blaze.android.plugin.AswbPlugin"/>
     <projectService serviceImplementation="com.google.idea.blaze.android.manifest.ManifestParser"/>
@@ -47,6 +46,10 @@
         implementation="com.google.idea.blaze.android.resources.actions.BlazeNewResourceCreationHandler" />
   </extensions>
 
+  <extensionPoints>
+    <extensionPoint qualifiedName="com.google.idea.blaze.BuildSystemAndroidJdkProvider" interface="com.google.idea.blaze.android.sync.BuildSystemAndroidJdkProvider"/>
+  </extensionPoints>
+
   <extensions defaultExtensionNs="com.google.idea.blaze">
     <SyncPlugin implementation="com.google.idea.blaze.android.sync.BlazeAndroidSyncPlugin"/>
     <SyncListener implementation="com.google.idea.blaze.android.sync.BlazeAndroidSyncListener"/>
@@ -57,6 +60,7 @@
     <PrefetchFileSource implementation="com.google.idea.blaze.android.sync.AndroidPrefetchFileSource"/>
     <BlazeCommandRunConfigurationHandlerProvider implementation="com.google.idea.blaze.android.run.binary.BlazeAndroidBinaryRunConfigurationHandlerProvider"/>
     <BlazeCommandRunConfigurationHandlerProvider implementation="com.google.idea.blaze.android.run.test.BlazeAndroidTestRunConfigurationHandlerProvider"/>
+    <BuildSystemAndroidJdkProvider implementation="com.google.idea.blaze.android.sync.BazelAndroidJdkProvider"/>
   </extensions>
 
   <extensions defaultExtensionNs="com.android.ide">
diff --git a/aswb/src/com/google/idea/blaze/android/cppapi/NdkSupport.java b/aswb/src/com/google/idea/blaze/android/cppapi/NdkSupport.java
index 506dfe6..8456626 100644
--- a/aswb/src/com/google/idea/blaze/android/cppapi/NdkSupport.java
+++ b/aswb/src/com/google/idea/blaze/android/cppapi/NdkSupport.java
@@ -19,5 +19,5 @@
 
 /** Contains the experiment that turns on NDK support */
 public class NdkSupport {
-  public static final BoolExperiment NDK_SUPPORT = new BoolExperiment("ndk.support", false);
+  public static final BoolExperiment NDK_SUPPORT = new BoolExperiment("ndk.support", true);
 }
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 3087e9a..f6b50ae 100644
--- a/aswb/src/com/google/idea/blaze/android/cppimpl/BlazeNdkSupportEnabler.java
+++ b/aswb/src/com/google/idea/blaze/android/cppimpl/BlazeNdkSupportEnabler.java
@@ -23,6 +23,7 @@
 import com.google.idea.blaze.base.projectview.ProjectViewSet;
 import com.google.idea.blaze.base.scope.BlazeContext;
 import com.google.idea.blaze.base.settings.BlazeImportSettings;
+import com.google.idea.blaze.base.sync.BlazeSyncParams.SyncMode;
 import com.google.idea.blaze.base.sync.SyncListener;
 import com.google.idea.blaze.cpp.BlazeCWorkspace;
 import com.intellij.openapi.application.ApplicationManager;
@@ -39,6 +40,7 @@
       BlazeImportSettings importSettings,
       ProjectViewSet projectViewSet,
       BlazeProjectData blazeProjectData,
+      SyncMode syncMode,
       SyncResult syncResult) {
     boolean enabled = blazeProjectData.workspaceLanguageSettings.isLanguageActive(LanguageClass.C);
     enableCSupportInIde(project, enabled);
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 becaa66..ddd7d49 100644
--- a/aswb/src/com/google/idea/blaze/android/manifest/ManifestParser.java
+++ b/aswb/src/com/google/idea/blaze/android/manifest/ManifestParser.java
@@ -20,6 +20,7 @@
 import com.google.idea.blaze.base.projectview.ProjectViewSet;
 import com.google.idea.blaze.base.scope.BlazeContext;
 import com.google.idea.blaze.base.settings.BlazeImportSettings;
+import com.google.idea.blaze.base.sync.BlazeSyncParams.SyncMode;
 import com.google.idea.blaze.base.sync.SyncListener;
 import com.intellij.openapi.application.ApplicationManager;
 import com.intellij.openapi.application.ModalityState;
@@ -105,6 +106,7 @@
         BlazeImportSettings importSettings,
         ProjectViewSet projectViewSet,
         BlazeProjectData blazeProjectData,
+        SyncMode syncMode,
         SyncResult syncResult) {
       getInstance(project).manifestFileMap.clear();
     }
diff --git a/aswb/src/com/google/idea/blaze/android/projectview/AndroidSdkPlatformSection.java b/aswb/src/com/google/idea/blaze/android/projectview/AndroidSdkPlatformSection.java
index b2c7177..056f692 100644
--- a/aswb/src/com/google/idea/blaze/android/projectview/AndroidSdkPlatformSection.java
+++ b/aswb/src/com/google/idea/blaze/android/projectview/AndroidSdkPlatformSection.java
@@ -15,13 +15,20 @@
  */
 package com.google.idea.blaze.android.projectview;
 
+import com.google.idea.blaze.android.sync.sdk.AndroidSdkFromProjectView;
+import com.google.idea.blaze.base.projectview.ProjectView;
 import com.google.idea.blaze.base.projectview.parser.ParseContext;
 import com.google.idea.blaze.base.projectview.parser.ProjectViewParser;
 import com.google.idea.blaze.base.projectview.section.ScalarSection;
 import com.google.idea.blaze.base.projectview.section.ScalarSectionParser;
 import com.google.idea.blaze.base.projectview.section.SectionKey;
 import com.google.idea.blaze.base.projectview.section.SectionParser;
+import com.google.idea.blaze.base.projectview.section.sections.TextBlock;
+import com.google.idea.blaze.base.projectview.section.sections.TextBlockSection;
+import com.intellij.openapi.projectRoots.Sdk;
 import com.intellij.openapi.util.text.StringUtil;
+import java.util.Collection;
+import org.jetbrains.android.sdk.AndroidSdkUtils;
 import org.jetbrains.annotations.Nullable;
 
 /** Allows manual override of the android sdk. */
@@ -50,5 +57,25 @@
     public ItemType getItemType() {
       return ItemType.Other;
     }
+
+    @Override
+    public ProjectView addProjectViewDefaultValue(ProjectView projectView) {
+      if (!projectView.getSectionsOfType(KEY).isEmpty()) {
+        return projectView;
+      }
+      Collection<Sdk> sdks = AndroidSdkUtils.getAllAndroidSdks();
+      return ProjectView.builder(projectView)
+          .add(TextBlockSection.of(TextBlock.newLine()))
+          .add(TextBlockSection.of(TextBlock.of("# Please set to an android SDK platform")))
+          .add(
+              TextBlockSection.of(
+                  TextBlock.of(
+                      sdks.isEmpty()
+                          ? "# You currently have no SDKs. Please use the SDK manager first."
+                          : "# Available SDKs are: "
+                              + AndroidSdkFromProjectView.getAvailableSdkPlatforms(sdks))))
+          .add(ScalarSection.builder(KEY).set("<android sdk platform>"))
+          .build();
+    }
   }
 }
diff --git a/aswb/src/com/google/idea/blaze/android/projectview/GeneratedAndroidResourcesSection.java b/aswb/src/com/google/idea/blaze/android/projectview/GeneratedAndroidResourcesSection.java
new file mode 100644
index 0000000..a93d218
--- /dev/null
+++ b/aswb/src/com/google/idea/blaze/android/projectview/GeneratedAndroidResourcesSection.java
@@ -0,0 +1,67 @@
+/*
+ * 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.projectview;
+
+import com.google.idea.blaze.base.projectview.parser.ParseContext;
+import com.google.idea.blaze.base.projectview.parser.ProjectViewParser;
+import com.google.idea.blaze.base.projectview.section.ListSection;
+import com.google.idea.blaze.base.projectview.section.ListSectionParser;
+import com.google.idea.blaze.base.projectview.section.SectionKey;
+import com.google.idea.blaze.base.projectview.section.SectionParser;
+import com.google.idea.blaze.base.ui.BlazeValidationError;
+import com.intellij.util.PathUtil;
+import java.util.ArrayList;
+import java.util.List;
+import javax.annotation.Nullable;
+
+/** Project view section white-listing generated resource directories */
+public class GeneratedAndroidResourcesSection {
+  public static final SectionKey<GenfilesPath, ListSection<GenfilesPath>> KEY =
+      SectionKey.of("generated_android_resource_directories");
+
+  public static final SectionParser PARSER = new Parser();
+
+  private GeneratedAndroidResourcesSection() {}
+
+  private static class Parser extends ListSectionParser<GenfilesPath> {
+    Parser() {
+      super(KEY);
+    }
+
+    @Nullable
+    @Override
+    protected GenfilesPath parseItem(ProjectViewParser parser, ParseContext parseContext) {
+      String canonicalPath = PathUtil.getCanonicalPath(parseContext.current().text);
+
+      List<BlazeValidationError> errors = new ArrayList<>();
+      if (!GenfilesPath.validate(canonicalPath, errors)) {
+        parseContext.addErrors(errors);
+        return null;
+      }
+      return new GenfilesPath(canonicalPath);
+    }
+
+    @Override
+    protected void printItem(GenfilesPath item, StringBuilder sb) {
+      sb.append(item.relativePath);
+    }
+
+    @Override
+    public ItemType getItemType() {
+      return ItemType.FileSystemItem;
+    }
+  }
+}
diff --git a/aswb/src/com/google/idea/blaze/android/projectview/GenfilesPath.java b/aswb/src/com/google/idea/blaze/android/projectview/GenfilesPath.java
new file mode 100644
index 0000000..90960f9
--- /dev/null
+++ b/aswb/src/com/google/idea/blaze/android/projectview/GenfilesPath.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.android.projectview;
+
+import com.google.common.base.Objects;
+import com.google.idea.blaze.base.ui.BlazeValidationError;
+import java.io.Serializable;
+import java.util.List;
+
+/** Project view entry data for {@link GeneratedAndroidResourcesSection}. */
+public class GenfilesPath implements Serializable {
+  private static final long serialVersionUID = 1L;
+
+  public final String relativePath;
+
+  public GenfilesPath(String relativePath) {
+    this.relativePath = relativePath;
+  }
+
+  static boolean validate(String path, List<BlazeValidationError> errors) {
+    if (path.startsWith("/")) {
+      BlazeValidationError.collect(
+          errors,
+          new BlazeValidationError(
+              "Genfiles path must be relative; cannot start with '/': " + path));
+      return false;
+    }
+
+    if (path.endsWith("/")) {
+      BlazeValidationError.collect(
+          errors, new BlazeValidationError("Genfiles path may not end with '/': " + path));
+      return false;
+    }
+
+    if (path.indexOf(':') >= 0) {
+      BlazeValidationError.collect(
+          errors, new BlazeValidationError("Genfiles path may not contain ':': " + path));
+      return false;
+    }
+    return true;
+  }
+
+  @Override
+  public boolean equals(Object o) {
+    if (this == o) {
+      return true;
+    }
+    if (o == null || getClass() != o.getClass()) {
+      return false;
+    }
+    GenfilesPath that = (GenfilesPath) o;
+    return Objects.equal(relativePath, that.relativePath);
+  }
+
+  @Override
+  public int hashCode() {
+    return Objects.hashCode(relativePath);
+  }
+
+  @Override
+  public String toString() {
+    return relativePath;
+  }
+}
diff --git a/aswb/src/com/google/idea/blaze/android/settings/AswbGlobalSettings.java b/aswb/src/com/google/idea/blaze/android/settings/AswbGlobalSettings.java
deleted file mode 100644
index 53577f5..0000000
--- a/aswb/src/com/google/idea/blaze/android/settings/AswbGlobalSettings.java
+++ /dev/null
@@ -1,55 +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.settings;
-
-import com.intellij.openapi.components.PersistentStateComponent;
-import com.intellij.openapi.components.ServiceManager;
-import com.intellij.openapi.components.State;
-import com.intellij.openapi.components.Storage;
-import com.intellij.util.xmlb.XmlSerializerUtil;
-import org.jetbrains.annotations.Nullable;
-
-/** Stores aswb global settings. */
-@State(name = "AswbGlobalSettings", storages = @Storage("aswb.global.xml"))
-public class AswbGlobalSettings implements PersistentStateComponent<AswbGlobalSettings> {
-
-  @Deprecated private String localSdkLocation;
-
-  public static AswbGlobalSettings getInstance() {
-    return ServiceManager.getService(AswbGlobalSettings.class);
-  }
-
-  @Nullable
-  @Override
-  public AswbGlobalSettings getState() {
-    return this;
-  }
-
-  @Override
-  public void loadState(AswbGlobalSettings state) {
-    XmlSerializerUtil.copyBean(state, this);
-  }
-
-  @Deprecated
-  public void setLocalSdkLocation(String localSdkLocation) {
-    this.localSdkLocation = localSdkLocation;
-  }
-
-  @Deprecated
-  public String getLocalSdkLocation() {
-    return localSdkLocation;
-  }
-}
diff --git a/aswb/src/com/google/idea/blaze/android/sync/BazelAndroidJdkProvider.java b/aswb/src/com/google/idea/blaze/android/sync/BazelAndroidJdkProvider.java
new file mode 100644
index 0000000..f45b368
--- /dev/null
+++ b/aswb/src/com/google/idea/blaze/android/sync/BazelAndroidJdkProvider.java
@@ -0,0 +1,33 @@
+/*
+ * Copyright 2016 The Bazel Authors. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.idea.blaze.android.sync;
+
+import com.google.idea.blaze.base.settings.Blaze;
+import com.google.idea.blaze.base.settings.Blaze.BuildSystem;
+import com.intellij.openapi.project.Project;
+import com.intellij.pom.java.LanguageLevel;
+import javax.annotation.Nullable;
+
+/** The highest JDK language level supported by bazel. */
+public class BazelAndroidJdkProvider implements BuildSystemAndroidJdkProvider {
+
+  @Nullable
+  @Override
+  public LanguageLevel getLanguageLevel(Project project) {
+    BuildSystem buildSystem = Blaze.getBuildSystem(project);
+    return buildSystem == BuildSystem.Bazel ? LanguageLevel.JDK_1_7 : null;
+  }
+}
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 e6240bd..084aec6 100644
--- a/aswb/src/com/google/idea/blaze/android/sync/BlazeAndroidJavaSyncAugmenter.java
+++ b/aswb/src/com/google/idea/blaze/android/sync/BlazeAndroidJavaSyncAugmenter.java
@@ -15,22 +15,27 @@
  */
 package com.google.idea.blaze.android.sync;
 
+import com.google.idea.blaze.android.projectview.GeneratedAndroidResourcesSection;
 import com.google.idea.blaze.android.sync.importer.BlazeAndroidWorkspaceImporter;
 import com.google.idea.blaze.base.ideinfo.AndroidIdeInfo;
 import com.google.idea.blaze.base.ideinfo.LibraryArtifact;
 import com.google.idea.blaze.base.ideinfo.TargetIdeInfo;
 import com.google.idea.blaze.base.model.primitives.Kind;
 import com.google.idea.blaze.base.model.primitives.LanguageClass;
+import com.google.idea.blaze.base.projectview.ProjectViewSet;
 import com.google.idea.blaze.base.sync.projectview.WorkspaceLanguageSettings;
 import com.google.idea.blaze.java.sync.BlazeJavaSyncAugmenter;
 import com.google.idea.blaze.java.sync.model.BlazeJarLibrary;
 import java.util.Collection;
+import java.util.Set;
+import java.util.stream.Collectors;
 
 /** Augments the java sync process with Android support. */
 public class BlazeAndroidJavaSyncAugmenter implements BlazeJavaSyncAugmenter {
   @Override
   public void addJarsForSourceTarget(
       WorkspaceLanguageSettings workspaceLanguageSettings,
+      ProjectViewSet projectViewSet,
       TargetIdeInfo target,
       Collection<BlazeJarLibrary> jars,
       Collection<BlazeJarLibrary> genJars) {
@@ -45,9 +50,15 @@
     if (idlJar != null) {
       genJars.add(new BlazeJarLibrary(idlJar, target.key));
     }
-
+    Set<String> whitelistedGenResourcePaths =
+        projectViewSet
+            .listItems(GeneratedAndroidResourcesSection.KEY)
+            .stream()
+            .map(genfilesPath -> genfilesPath.relativePath)
+            .collect(Collectors.toSet());
     if (BlazeAndroidWorkspaceImporter.shouldGenerateResources(androidIdeInfo)
-        && !BlazeAndroidWorkspaceImporter.shouldGenerateResourceModule(androidIdeInfo)) {
+        && !BlazeAndroidWorkspaceImporter.shouldGenerateResourceModule(
+            androidIdeInfo, whitelistedGenResourcePaths)) {
       // Add blaze's output unless it's a top level rule.
       // In these cases the resource jar contains the entire
       // transitive closure of R classes. It's unlikely this is wanted to resolve in the IDE.
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 fda9123..815c4c9 100644
--- a/aswb/src/com/google/idea/blaze/android/sync/BlazeAndroidSyncListener.java
+++ b/aswb/src/com/google/idea/blaze/android/sync/BlazeAndroidSyncListener.java
@@ -17,6 +17,7 @@
 
 import com.android.tools.idea.res.ResourceFolderRegistry;
 import com.google.idea.blaze.base.scope.BlazeContext;
+import com.google.idea.blaze.base.sync.BlazeSyncParams.SyncMode;
 import com.google.idea.blaze.base.sync.SyncListener;
 import com.intellij.openapi.project.DumbService;
 import com.intellij.openapi.project.Project;
@@ -24,7 +25,8 @@
 /** Android-specific hooks to run after a blaze sync. */
 public class BlazeAndroidSyncListener extends SyncListener.Adapter {
   @Override
-  public void afterSync(Project project, BlazeContext context, SyncResult syncResult) {
+  public void afterSync(
+      Project project, BlazeContext context, SyncMode syncMode, 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 52c21b2..72b352d 100644
--- a/aswb/src/com/google/idea/blaze/android/sync/BlazeAndroidSyncPlugin.java
+++ b/aswb/src/com/google/idea/blaze/android/sync/BlazeAndroidSyncPlugin.java
@@ -16,20 +16,17 @@
 package com.google.idea.blaze.android.sync;
 
 import com.android.tools.idea.sdk.IdeSdks;
-import com.google.common.base.Joiner;
-import com.google.common.base.Strings;
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableSet;
 import com.google.idea.blaze.android.cppapi.NdkSupport;
 import com.google.idea.blaze.android.projectview.AndroidSdkPlatformSection;
+import com.google.idea.blaze.android.projectview.GeneratedAndroidResourcesSection;
 import com.google.idea.blaze.android.sync.importer.BlazeAndroidWorkspaceImporter;
 import com.google.idea.blaze.android.sync.model.AndroidSdkPlatform;
 import com.google.idea.blaze.android.sync.model.BlazeAndroidImportResult;
 import com.google.idea.blaze.android.sync.model.BlazeAndroidSyncData;
 import com.google.idea.blaze.android.sync.projectstructure.BlazeAndroidProjectStructureSyncer;
 import com.google.idea.blaze.android.sync.sdk.AndroidSdkFromProjectView;
-import com.google.idea.blaze.android.sync.sdk.SdkExperiment;
-import com.google.idea.blaze.android.sync.sdklegacy.AndroidSdkPlatformSyncer;
 import com.google.idea.blaze.base.ideinfo.TargetMap;
 import com.google.idea.blaze.base.model.BlazeProjectData;
 import com.google.idea.blaze.base.model.SyncState;
@@ -43,9 +40,8 @@
 import com.google.idea.blaze.base.scope.output.IssueOutput;
 import com.google.idea.blaze.base.scope.output.StatusOutput;
 import com.google.idea.blaze.base.scope.scopes.TimingScope;
-import com.google.idea.blaze.base.settings.Blaze;
-import com.google.idea.blaze.base.settings.Blaze.BuildSystem;
 import com.google.idea.blaze.base.sync.BlazeSyncPlugin;
+import com.google.idea.blaze.base.sync.SourceFolderProvider;
 import com.google.idea.blaze.base.sync.libraries.LibrarySource;
 import com.google.idea.blaze.base.sync.projectview.WorkspaceLanguageSettings;
 import com.google.idea.blaze.base.sync.workspace.ArtifactLocationDecoder;
@@ -53,6 +49,8 @@
 import com.google.idea.blaze.base.sync.workspace.WorkingSet;
 import com.google.idea.blaze.base.sync.workspace.WorkspacePathResolver;
 import com.google.idea.blaze.java.projectview.JavaLanguageLevelSection;
+import com.google.idea.blaze.java.sync.model.BlazeJavaSyncData;
+import com.google.idea.blaze.java.sync.projectstructure.JavaSourceFolderProvider;
 import com.intellij.openapi.application.ApplicationManager;
 import com.intellij.openapi.application.ModalityState;
 import com.intellij.openapi.module.Module;
@@ -78,7 +76,7 @@
 
   @Override
   public ImmutableList<WorkspaceType> getSupportedWorkspaceTypes() {
-    return ImmutableList.of(WorkspaceType.ANDROID, WorkspaceType.ANDROID_NDK);
+    return ImmutableList.of(WorkspaceType.ANDROID);
   }
 
   @Nullable
@@ -90,7 +88,7 @@
   @Nullable
   @Override
   public ModuleType getWorkspaceModuleType(WorkspaceType workspaceType) {
-    if (workspaceType == WorkspaceType.ANDROID || workspaceType == WorkspaceType.ANDROID_NDK) {
+    if (workspaceType == WorkspaceType.ANDROID) {
       return StdModuleTypes.JAVA;
     }
     return null;
@@ -98,17 +96,13 @@
 
   @Override
   public Set<LanguageClass> getSupportedLanguagesInWorkspace(WorkspaceType workspaceType) {
-    switch (workspaceType) {
-      case ANDROID:
-        return ImmutableSet.of(LanguageClass.ANDROID, LanguageClass.JAVA);
-      case ANDROID_NDK:
-        if (NdkSupport.NDK_SUPPORT.getValue()) {
-          return ImmutableSet.of(LanguageClass.ANDROID, LanguageClass.JAVA, LanguageClass.C);
-        } else {
-          return ImmutableSet.of(LanguageClass.ANDROID, LanguageClass.JAVA);
-        }
-      default:
-        return ImmutableSet.of();
+    if (workspaceType != WorkspaceType.ANDROID) {
+      return ImmutableSet.of();
+    }
+    if (NdkSupport.NDK_SUPPORT.getValue()) {
+      return ImmutableSet.of(LanguageClass.ANDROID, LanguageClass.JAVA, LanguageClass.C);
+    } else {
+      return ImmutableSet.of(LanguageClass.ANDROID, LanguageClass.JAVA);
     }
   }
 
@@ -144,16 +138,12 @@
       return;
     }
 
-    final AndroidSdkPlatform androidSdkPlatform;
-    if (SdkExperiment.useStandardSdkManager()) {
-      androidSdkPlatform = AndroidSdkFromProjectView.getAndroidSdkPlatform(context, projectViewSet);
-    } else {
-      androidSdkPlatform = AndroidSdkPlatformSyncer.getAndroidSdkPlatform(project, context);
-    }
+    AndroidSdkPlatform androidSdkPlatform =
+        AndroidSdkFromProjectView.getAndroidSdkPlatform(context, projectViewSet);
 
     BlazeAndroidWorkspaceImporter workspaceImporter =
         new BlazeAndroidWorkspaceImporter(
-            project, context, workspaceRoot, projectViewSet, targetMap);
+            project, context, workspaceRoot, projectViewSet, targetMap, artifactLocationDecoder);
     BlazeAndroidImportResult importResult =
         Scope.push(
             context,
@@ -190,10 +180,7 @@
       return;
     }
 
-    LanguageLevel defaultLanguageLevel =
-        Blaze.getBuildSystem(project) == BuildSystem.Blaze
-            ? LanguageLevel.JDK_1_8
-            : LanguageLevel.JDK_1_7;
+    LanguageLevel defaultLanguageLevel = BuildSystemAndroidJdkProvider.languageLevel(project);
     LanguageLevel javaLanguageLevel =
         JavaLanguageLevelSection.getLanguageLevel(projectViewSet, defaultLanguageLevel);
     setProjectSdkAndLanguageLevel(project, sdk, javaLanguageLevel);
@@ -222,6 +209,15 @@
         isAndroidWorkspace(blazeProjectData.workspaceLanguageSettings));
   }
 
+  @Nullable
+  @Override
+  public SourceFolderProvider getSourceFolderProvider(BlazeProjectData projectData) {
+    if (!projectData.workspaceLanguageSettings.isWorkspaceType(WorkspaceType.ANDROID)) {
+      return null;
+    }
+    return new JavaSourceFolderProvider(projectData.syncState.get(BlazeJavaSyncData.class));
+  }
+
   @Override
   public boolean validate(
       Project project, BlazeContext context, BlazeProjectData blazeProjectData) {
@@ -249,33 +245,15 @@
       return true;
     }
 
-    if (workspaceLanguageSettings.isWorkspaceType(WorkspaceType.ANDROID_NDK)
+    if (workspaceLanguageSettings.isLanguageActive(LanguageClass.C)
         && !NdkSupport.NDK_SUPPORT.getValue()) {
       IssueOutput.error("Android NDK is not supported yet.").submit(context);
       return false;
     }
 
-    if (SdkExperiment.useStandardSdkManager()) {
-      if (AndroidSdkFromProjectView.getAndroidSdkPlatform(context, projectViewSet) == null) {
-        return false;
-      }
-    } else {
-      String androidSdkPlatform = projectViewSet.getScalarValue(AndroidSdkPlatformSection.KEY);
-      if (Strings.isNullOrEmpty(androidSdkPlatform)) {
-        String error =
-            Joiner.on('\n')
-                .join(
-                    "No android_sdk_platform set.",
-                    "You should specify the android SDK platform in your '.blazeproject' file.",
-                    "To set this add an 'android_sdk_platform' line to your .blazeproject file,",
-                    "e.g. 'android_sdk_platform: \"android-N\"', where 'android-N' is a",
-                    "platform directory name in your local SDK directory.");
-        IssueOutput.error(error)
-            .inFile(projectViewSet.getTopLevelProjectViewFile().projectViewFile)
-            .submit(context);
-      }
+    if (AndroidSdkFromProjectView.getAndroidSdkPlatform(context, projectViewSet) == null) {
+      return false;
     }
-
     return true;
   }
 
@@ -298,7 +276,8 @@
 
   @Override
   public Collection<SectionParser> getSections() {
-    return ImmutableList.of(AndroidSdkPlatformSection.PARSER);
+    return ImmutableList.of(
+        AndroidSdkPlatformSection.PARSER, GeneratedAndroidResourcesSection.PARSER);
   }
 
   @Nullable
@@ -311,7 +290,6 @@
   }
 
   private static boolean isAndroidWorkspace(WorkspaceLanguageSettings workspaceLanguageSettings) {
-    return workspaceLanguageSettings.isWorkspaceType(
-        WorkspaceType.ANDROID, WorkspaceType.ANDROID_NDK);
+    return workspaceLanguageSettings.isWorkspaceType(WorkspaceType.ANDROID);
   }
 }
diff --git a/aswb/src/com/google/idea/blaze/android/sync/BuildSystemAndroidJdkProvider.java b/aswb/src/com/google/idea/blaze/android/sync/BuildSystemAndroidJdkProvider.java
new file mode 100644
index 0000000..d9b53bf
--- /dev/null
+++ b/aswb/src/com/google/idea/blaze/android/sync/BuildSystemAndroidJdkProvider.java
@@ -0,0 +1,47 @@
+/*
+ * Copyright 2016 The Bazel Authors. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.idea.blaze.android.sync;
+
+import com.intellij.openapi.extensions.ExtensionPointName;
+import com.intellij.openapi.project.Project;
+import com.intellij.pom.java.LanguageLevel;
+import javax.annotation.Nullable;
+
+/** Provides the highest JDK language level supported by the build system. */
+public interface BuildSystemAndroidJdkProvider {
+
+  ExtensionPointName<BuildSystemAndroidJdkProvider> EP_NAME =
+      ExtensionPointName.create("com.google.idea.blaze.BuildSystemAndroidJdkProvider");
+
+  LanguageLevel DEFAULT_LANGUAGE_LEVEL = LanguageLevel.JDK_1_7;
+
+  static LanguageLevel languageLevel(Project project) {
+    for (BuildSystemAndroidJdkProvider provider : EP_NAME.getExtensions()) {
+      LanguageLevel level = provider.getLanguageLevel(project);
+      if (level != null) {
+        return level;
+      }
+    }
+    return DEFAULT_LANGUAGE_LEVEL;
+  }
+
+  /**
+   * Returns the highest JDK language level supported for this project, or null if the provider is
+   * unable to determine it.
+   */
+  @Nullable
+  LanguageLevel getLanguageLevel(Project project);
+}
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 7a3651b..3cb5351 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
@@ -18,10 +18,13 @@
 import com.google.common.collect.ArrayListMultimap;
 import com.google.common.collect.ComparisonChain;
 import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableSet;
 import com.google.common.collect.Lists;
 import com.google.common.collect.Multimap;
 import com.google.common.collect.Sets;
+import com.google.idea.blaze.android.projectview.GeneratedAndroidResourcesSection;
 import com.google.idea.blaze.android.sync.importer.aggregators.TransitiveResourceMap;
+import com.google.idea.blaze.android.sync.importer.problems.GeneratedResourceWarnings;
 import com.google.idea.blaze.android.sync.model.AndroidResourceModule;
 import com.google.idea.blaze.android.sync.model.BlazeAndroidImportResult;
 import com.google.idea.blaze.android.sync.model.BlazeResourceLibrary;
@@ -37,6 +40,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.ProjectViewTargetImportFilter;
+import com.google.idea.blaze.base.sync.workspace.ArtifactLocationDecoder;
 import com.intellij.openapi.project.Project;
 import java.util.Collection;
 import java.util.Collections;
@@ -44,25 +48,39 @@
 import java.util.Set;
 import java.util.stream.Collectors;
 import java.util.stream.Stream;
-import org.jetbrains.annotations.NotNull;
 import org.jetbrains.annotations.Nullable;
 
 /** Builds a BlazeWorkspace. */
 public final class BlazeAndroidWorkspaceImporter {
 
+  private final Project project;
   private final BlazeContext context;
   private final TargetMap targetMap;
   private final ProjectViewTargetImportFilter importFilter;
+  private final ProjectViewSet projectViewSet;
+  private final ArtifactLocationDecoder artifactLocationDecoder;
+  private final ImmutableSet<String> whitelistedGenResourcePaths;
 
   public BlazeAndroidWorkspaceImporter(
       Project project,
       BlazeContext context,
       WorkspaceRoot workspaceRoot,
       ProjectViewSet projectViewSet,
-      TargetMap targetMap) {
+      TargetMap targetMap,
+      ArtifactLocationDecoder artifactLocationDecoder) {
     this.context = context;
     this.targetMap = targetMap;
     this.importFilter = new ProjectViewTargetImportFilter(project, workspaceRoot, projectViewSet);
+    this.projectViewSet = projectViewSet;
+    this.artifactLocationDecoder = artifactLocationDecoder;
+    this.project = project;
+    this.whitelistedGenResourcePaths =
+        ImmutableSet.copyOf(
+            projectViewSet
+                .listItems(GeneratedAndroidResourcesSection.KEY)
+                .stream()
+                .map(genfilesPath -> genfilesPath.relativePath)
+                .collect(Collectors.toSet()));
   }
 
   public BlazeAndroidImportResult importWorkspace() {
@@ -84,7 +102,13 @@
       addSourceTarget(workspaceBuilder, transitiveResourceMap, target);
     }
 
-    warnAboutGeneratedResources(workspaceBuilder.generatedResourceLocations);
+    GeneratedResourceWarnings.submit(
+        project,
+        context,
+        projectViewSet,
+        artifactLocationDecoder,
+        workspaceBuilder.generatedResourceLocations,
+        whitelistedGenResourcePaths);
 
     ImmutableList<AndroidResourceModule> androidResourceModules =
         buildAndroidResourceModules(workspaceBuilder);
@@ -99,7 +123,8 @@
       TargetIdeInfo target) {
     AndroidIdeInfo androidIdeInfo = target.androidIdeInfo;
     assert androidIdeInfo != null;
-    if (shouldGenerateResources(androidIdeInfo) && shouldGenerateResourceModule(androidIdeInfo)) {
+    if (shouldGenerateResources(androidIdeInfo)
+        && shouldGenerateResourceModule(androidIdeInfo, whitelistedGenResourcePaths)) {
       AndroidResourceModule.Builder builder = new AndroidResourceModule.Builder(target.key);
       workspaceBuilder.androidResourceModules.add(builder);
 
@@ -108,6 +133,11 @@
           builder.addResource(artifactLocation);
         } else {
           workspaceBuilder.generatedResourceLocations.add(artifactLocation);
+          if (whitelistedGenResourcePaths.contains(artifactLocation.relativePath)) {
+            // Still track location in generatedResourceLocations, so that we can warn if a
+            // whitelist entry goes unused and can be removed.
+            builder.addResource(artifactLocation);
+          }
         }
       }
 
@@ -118,6 +148,9 @@
           builder.addTransitiveResource(artifactLocation);
         } else {
           workspaceBuilder.generatedResourceLocations.add(artifactLocation);
+          if (whitelistedGenResourcePaths.contains(artifactLocation.relativePath)) {
+            builder.addTransitiveResource(artifactLocation);
+          }
         }
       }
       for (TargetKey resourceDependency : transitiveResourceInfo.transitiveResourceTargets) {
@@ -137,19 +170,18 @@
     return androidIdeInfo.generateResourceClass && androidIdeInfo.legacyResources == null;
   }
 
-  public static boolean shouldGenerateResourceModule(AndroidIdeInfo androidIdeInfo) {
-    return androidIdeInfo.resources.stream().anyMatch(ArtifactLocation::isSource);
+  public static boolean shouldGenerateResourceModule(
+      AndroidIdeInfo androidIdeInfo, Set<String> whitelistedGenResourcePaths) {
+    return androidIdeInfo
+        .resources
+        .stream()
+        .anyMatch(location -> isSourceOrWhitelistedGenPath(location, whitelistedGenResourcePaths));
   }
 
-  private void warnAboutGeneratedResources(Set<ArtifactLocation> generatedResourceLocations) {
-    for (ArtifactLocation artifactLocation : generatedResourceLocations) {
-      IssueOutput.warn(
-              String.format(
-                  "Dropping generated resource directory '%s', "
-                      + "R classes will not contain resources from this directory",
-                  artifactLocation.getExecutionRootRelativePath()))
-          .submit(context);
-    }
+  private static boolean isSourceOrWhitelistedGenPath(
+      ArtifactLocation artifactLocation, Set<String> whitelistedGenResourcePaths) {
+    return artifactLocation.isSource()
+        || whitelistedGenResourcePaths.contains(artifactLocation.getRelativePath());
   }
 
   @Nullable
@@ -169,7 +201,6 @@
     return null;
   }
 
-  @NotNull
   private ImmutableList<AndroidResourceModule> buildAndroidResourceModules(
       WorkspaceBuilder workspaceBuilder) {
     // Filter empty resource modules
diff --git a/aswb/src/com/google/idea/blaze/android/sync/importer/problems/AddGeneratedResourceDirectoryNavigatable.java b/aswb/src/com/google/idea/blaze/android/sync/importer/problems/AddGeneratedResourceDirectoryNavigatable.java
new file mode 100644
index 0000000..59839bf
--- /dev/null
+++ b/aswb/src/com/google/idea/blaze/android/sync/importer/problems/AddGeneratedResourceDirectoryNavigatable.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.android.sync.importer.problems;
+
+import com.google.idea.blaze.android.projectview.GeneratedAndroidResourcesSection;
+import com.google.idea.blaze.android.projectview.GenfilesPath;
+import com.google.idea.blaze.base.ideinfo.ArtifactLocation;
+import com.google.idea.blaze.base.projectview.ProjectViewEdit;
+import com.google.idea.blaze.base.projectview.section.ListSection;
+import com.intellij.openapi.fileEditor.FileEditorManager;
+import com.intellij.openapi.fileEditor.OpenFileDescriptor;
+import com.intellij.openapi.project.Project;
+import com.intellij.openapi.ui.Messages;
+import com.intellij.openapi.vfs.VfsUtil;
+import com.intellij.openapi.vfs.VirtualFile;
+import com.intellij.pom.Navigatable;
+import java.io.File;
+
+/**
+ * Action to whitelist a generated resource directory in the local project view file.
+ *
+ * <p>This is unfortunately wrapped in a navigatable, because the IntelliJ ProblemsView class
+ * doesn't have an extension point or action group ID for extending the right-click actions.
+ *
+ * <p>Ideally, we would have a quick fix in the java R.type.foo and xml @type/foo references, but to
+ * know which generated directory contains type.foo, we would need to parse the resources, which
+ * would be expensive.
+ */
+class AddGeneratedResourceDirectoryNavigatable implements Navigatable {
+
+  private final Project project;
+  private final File projectViewFile;
+  private final ArtifactLocation generatedResDir;
+
+  AddGeneratedResourceDirectoryNavigatable(
+      Project project, File projectViewFile, ArtifactLocation generatedResDir) {
+    this.project = project;
+    this.projectViewFile = projectViewFile;
+    this.generatedResDir = generatedResDir;
+  }
+
+  @Override
+  public void navigate(boolean requestFocus) {
+    int addToProjectView =
+        Messages.showYesNoDialog(
+            String.format(
+                "Whitelist generated resource directory \"%s\" in project view?",
+                generatedResDir.getRelativePath()),
+            "Whitelist generated resource",
+            null);
+    if (addToProjectView == Messages.YES) {
+      addDirectoryToProjectView(project, projectViewFile, generatedResDir);
+    }
+  }
+
+  @Override
+  public boolean canNavigate() {
+    return true;
+  }
+
+  @Override
+  public boolean canNavigateToSource() {
+    return false;
+  }
+
+  private static void addDirectoryToProjectView(
+      Project project, File projectViewFile, ArtifactLocation generatedResDir) {
+    ProjectViewEdit edit =
+        ProjectViewEdit.editLocalProjectView(
+            project,
+            builder -> {
+              ListSection<GenfilesPath> existingSection =
+                  builder.getLast(GeneratedAndroidResourcesSection.KEY);
+              ListSection.Builder<GenfilesPath> directoryBuilder =
+                  ListSection.update(GeneratedAndroidResourcesSection.KEY, existingSection);
+              directoryBuilder.add(new GenfilesPath(generatedResDir.getRelativePath()));
+              builder.replace(existingSection, directoryBuilder);
+              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();
+    VirtualFile projectView = VfsUtil.findFileByIoFile(projectViewFile, false);
+    if (projectView != null) {
+      FileEditorManager.getInstance(project)
+          .openEditor(new OpenFileDescriptor(project, projectView), true);
+    }
+  }
+}
diff --git a/aswb/src/com/google/idea/blaze/android/sync/importer/problems/GeneratedResourceClassifier.java b/aswb/src/com/google/idea/blaze/android/sync/importer/problems/GeneratedResourceClassifier.java
new file mode 100644
index 0000000..10ec342
--- /dev/null
+++ b/aswb/src/com/google/idea/blaze/android/sync/importer/problems/GeneratedResourceClassifier.java
@@ -0,0 +1,177 @@
+/*
+ * Copyright 2016 The Bazel Authors. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.idea.blaze.android.sync.importer.problems;
+
+import com.android.SdkConstants;
+import com.android.ide.common.resources.configuration.FolderConfiguration;
+import com.android.ide.common.resources.configuration.LocaleQualifier;
+import com.android.ide.common.resources.configuration.ResourceQualifier;
+import com.android.resources.ResourceFolderType;
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.collect.ImmutableSortedMap;
+import com.google.common.util.concurrent.Futures;
+import com.google.common.util.concurrent.ListenableFuture;
+import com.google.common.util.concurrent.ListeningExecutorService;
+import com.google.idea.blaze.base.ideinfo.ArtifactLocation;
+import com.google.idea.blaze.base.io.FileAttributeProvider;
+import com.google.idea.blaze.base.sync.workspace.ArtifactLocationDecoder;
+import com.intellij.openapi.diagnostic.Logger;
+import java.io.File;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.List;
+import java.util.concurrent.ExecutionException;
+import java.util.stream.Collectors;
+
+/**
+ * Classifies generated resource directories as "interesting" or not.
+ *
+ * <p>Uninteresting resources: It is very common to have string translations as the only generated
+ * resources for a rule. If that's the case, we can ignore them since we likely already have a
+ * source variant of the same resource as a baseline for resolving references in Java or XML.
+ *
+ * <p>Other generated resources are interesting.
+ */
+class GeneratedResourceClassifier {
+
+  private static final Logger logger = Logger.getInstance(GeneratedResourceClassifier.class);
+
+  private final ImmutableSortedMap<ArtifactLocation, Integer> interestingDirectories;
+
+  GeneratedResourceClassifier(
+      Collection<ArtifactLocation> generatedResourceLocations,
+      ArtifactLocationDecoder artifactLocationDecoder,
+      ListeningExecutorService executorService) {
+    FileAttributeProvider fileAttributeProvider = FileAttributeProvider.getInstance();
+    List<ListenableFuture<GenResourceClassification>> jobs =
+        generatedResourceLocations
+            .stream()
+            .map(
+                location ->
+                    executorService.submit(
+                        () ->
+                            classifyLocation(
+                                location, artifactLocationDecoder, fileAttributeProvider)))
+            .collect(Collectors.toList());
+
+    ImmutableSortedMap.Builder<ArtifactLocation, Integer> interesting =
+        ImmutableSortedMap.naturalOrder();
+    try {
+      for (GenResourceClassification classification : Futures.allAsList(jobs).get()) {
+        if (classification.isInteresting) {
+          interesting.put(classification.artifactLocation, classification.numSubDirs);
+        }
+      }
+    } catch (InterruptedException e) {
+      Thread.currentThread().interrupt();
+      interesting = considerAllInteresting(generatedResourceLocations);
+    } catch (ExecutionException e) {
+      logger.error(e);
+      interesting = considerAllInteresting(generatedResourceLocations);
+    } finally {
+      interestingDirectories = interesting.build();
+    }
+  }
+
+  /**
+   * Returns a collection of interesting directories as a sorted map from directory to estimated
+   * size of subdirectory (to estimate cost of indexing).
+   *
+   * @return map of interesting directories and associated sizes
+   */
+  public ImmutableSortedMap<ArtifactLocation, Integer> getInterestingDirectories() {
+    return interestingDirectories;
+  }
+
+  private static ImmutableSortedMap.Builder<ArtifactLocation, Integer> considerAllInteresting(
+      Collection<ArtifactLocation> generatedResourceLocations) {
+    ImmutableSortedMap.Builder<ArtifactLocation, Integer> builder =
+        ImmutableSortedMap.naturalOrder();
+    for (ArtifactLocation location : generatedResourceLocations) {
+      builder.put(location, -1);
+    }
+    return builder;
+  }
+
+  private static class GenResourceClassification {
+    final boolean isInteresting;
+    final ArtifactLocation artifactLocation;
+    final int numSubDirs;
+
+    GenResourceClassification(
+        boolean isInteresting, ArtifactLocation artifactLocation, int numSubDirs) {
+      this.isInteresting = isInteresting;
+      this.artifactLocation = artifactLocation;
+      this.numSubDirs = numSubDirs;
+    }
+
+    static GenResourceClassification uninteresting(
+        ArtifactLocation artifactLocation, int numSubDirs) {
+      return new GenResourceClassification(false, artifactLocation, numSubDirs);
+    }
+
+    static GenResourceClassification interesting(
+        ArtifactLocation artifactLocation, int numSubDirs) {
+      return new GenResourceClassification(true, artifactLocation, numSubDirs);
+    }
+  }
+
+  private static GenResourceClassification classifyLocation(
+      ArtifactLocation artifactLocation,
+      ArtifactLocationDecoder artifactLocationDecoder,
+      FileAttributeProvider fileAttributeProvider) {
+    File resDirectory = artifactLocationDecoder.decode(artifactLocation);
+    File[] children = fileAttributeProvider.listFiles(resDirectory);
+    if (children == null) {
+      return GenResourceClassification.uninteresting(artifactLocation, 0);
+    }
+    if (mayHaveNonStringTranslations(children)) {
+      return GenResourceClassification.interesting(artifactLocation, children.length);
+    } else {
+      return GenResourceClassification.uninteresting(artifactLocation, children.length);
+    }
+  }
+
+  @VisibleForTesting
+  static boolean mayHaveNonStringTranslations(File[] resDirectoryChildren) {
+    return Arrays.stream(resDirectoryChildren)
+        .anyMatch(child -> mayHaveNonStringTranslations(child.getName()));
+  }
+
+  private static boolean mayHaveNonStringTranslations(String dirName) {
+    // String translations only sit in the values-xx-rYY directories, so we can rule out other
+    // directories quickly.
+    if (!dirName.contains(SdkConstants.RES_QUALIFIER_SEP)) {
+      return true;
+    }
+    if (ResourceFolderType.getFolderType(dirName) != ResourceFolderType.VALUES) {
+      return true;
+    }
+    FolderConfiguration config = FolderConfiguration.getConfigForFolder(dirName);
+    // Conservatively say it's interesting if there is an unrecognized configuration.
+    if (config == null) {
+      return true;
+    }
+    // If this is a translation mixed with something else, consider it a translation directory.
+    boolean hasTranslation = false;
+    for (ResourceQualifier qualifier : config.getQualifiers()) {
+      if (qualifier instanceof LocaleQualifier) {
+        hasTranslation = true;
+      }
+    }
+    return !hasTranslation;
+  }
+}
diff --git a/aswb/src/com/google/idea/blaze/android/sync/importer/problems/GeneratedResourceWarnings.java b/aswb/src/com/google/idea/blaze/android/sync/importer/problems/GeneratedResourceWarnings.java
new file mode 100644
index 0000000..1e6afae
--- /dev/null
+++ b/aswb/src/com/google/idea/blaze/android/sync/importer/problems/GeneratedResourceWarnings.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.android.sync.importer.problems;
+
+import com.google.common.collect.ImmutableSortedMap;
+import com.google.idea.blaze.android.projectview.GeneratedAndroidResourcesSection;
+import com.google.idea.blaze.base.async.executor.BlazeExecutor;
+import com.google.idea.blaze.base.ideinfo.ArtifactLocation;
+import com.google.idea.blaze.base.projectview.ProjectViewSet;
+import com.google.idea.blaze.base.scope.BlazeContext;
+import com.google.idea.blaze.base.scope.output.IssueOutput;
+import com.google.idea.blaze.base.sync.workspace.ArtifactLocationDecoder;
+import com.intellij.openapi.project.Project;
+import java.io.File;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Set;
+
+/** Submits generated resource warnings and potential fixes to the problems view. */
+public class GeneratedResourceWarnings {
+
+  private GeneratedResourceWarnings() {}
+
+  public static void submit(
+      Project project,
+      BlazeContext context,
+      ProjectViewSet projectViewSet,
+      ArtifactLocationDecoder artifactLocationDecoder,
+      Set<ArtifactLocation> generatedResourceLocations,
+      Set<String> whitelistedLocations) {
+    if (generatedResourceLocations.isEmpty()) {
+      return;
+    }
+    Set<ArtifactLocation> nonWhitelistedLocations = new HashSet<>();
+    Set<String> unusedWhitelistEntries = new HashSet<>();
+    filterWhitelistedEntries(
+        generatedResourceLocations,
+        whitelistedLocations,
+        nonWhitelistedLocations,
+        unusedWhitelistEntries);
+    // Tag any warnings with the project view file.
+    File projectViewFile = projectViewSet.getTopLevelProjectViewFile().projectViewFile;
+    if (!nonWhitelistedLocations.isEmpty()) {
+      GeneratedResourceClassifier classifier =
+          new GeneratedResourceClassifier(
+              nonWhitelistedLocations,
+              artifactLocationDecoder,
+              BlazeExecutor.getInstance().getExecutor());
+      ImmutableSortedMap<ArtifactLocation, Integer> interestingDirectories =
+          classifier.getInterestingDirectories();
+      if (!interestingDirectories.isEmpty()) {
+        IssueOutput.warn(
+                String.format(
+                    "Dropping %d generated resource directories.\n"
+                        + "R classes will not contain resources from these directories.\n"
+                        + "Double-click to add to project view if needed to resolve references.",
+                    interestingDirectories.size()))
+            .inFile(projectViewFile)
+            .onLine(1)
+            .inColumn(1)
+            .submit(context);
+        for (Map.Entry<ArtifactLocation, Integer> entry : interestingDirectories.entrySet()) {
+          IssueOutput.warn(
+                  String.format(
+                      "Dropping generated resource directory '%s' w/ %d subdirs",
+                      entry.getKey(), entry.getValue()))
+              .inFile(projectViewFile)
+              .navigatable(
+                  new AddGeneratedResourceDirectoryNavigatable(
+                      project, projectViewFile, entry.getKey()))
+              .submit(context);
+        }
+      }
+    }
+    // Warn about unused parts of the whitelist.
+    if (!unusedWhitelistEntries.isEmpty()) {
+      IssueOutput.warn(
+              String.format(
+                  "%d unused entries in project view section \"%s\":\n%s",
+                  unusedWhitelistEntries.size(),
+                  GeneratedAndroidResourcesSection.KEY.getName(),
+                  String.join("\n  ", unusedWhitelistEntries)))
+          .inFile(projectViewFile)
+          .submit(context);
+    }
+  }
+
+  private static void filterWhitelistedEntries(
+      Set<ArtifactLocation> generatedResourceLocations,
+      Set<String> whitelistedLocations,
+      Set<ArtifactLocation> nonWhitelistedLocations,
+      Set<String> unusedWhitelistEntries) {
+    unusedWhitelistEntries.addAll(whitelistedLocations);
+    for (ArtifactLocation location : generatedResourceLocations) {
+      if (whitelistedLocations.contains(location.getRelativePath())) {
+        unusedWhitelistEntries.remove(location.getRelativePath());
+      } else {
+        nonWhitelistedLocations.add(location);
+      }
+    }
+  }
+}
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 76d4767..20d5462 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
@@ -19,6 +19,7 @@
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.Maps;
 import com.google.common.collect.Sets;
+import com.google.idea.blaze.android.projectview.GeneratedAndroidResourcesSection;
 import com.google.idea.blaze.android.resources.LightResourceClassService;
 import com.google.idea.blaze.android.run.BlazeAndroidRunConfigurationHandler;
 import com.google.idea.blaze.android.sync.model.AndroidResourceModule;
@@ -190,13 +191,17 @@
           ++totalRunConfigurationModules;
         }
 
+        int whitelistedGenResources =
+            projectViewSet.listItems(GeneratedAndroidResourcesSection.KEY).size();
         context.output(
             PrintOutput.log(
                 String.format(
-                    "Android resource module count: %d, run config modules: %d, order entries: %d",
+                    "Android resource module count: %d, run config modules: %d, order entries: %d, "
+                        + "generated resources: %d",
                     syncData.importResult.androidResourceModules.size(),
                     totalRunConfigurationModules,
-                    totalOrderEntries)));
+                    totalOrderEntries,
+                    whitelistedGenResources)));
       }
     } else {
       AndroidFacetModuleCustomizer.removeAndroidFacet(workspaceModule);
diff --git a/aswb/src/com/google/idea/blaze/android/sync/sdk/AndroidSdkFromProjectView.java b/aswb/src/com/google/idea/blaze/android/sync/sdk/AndroidSdkFromProjectView.java
index 2740352..05b52e4 100644
--- a/aswb/src/com/google/idea/blaze/android/sync/sdk/AndroidSdkFromProjectView.java
+++ b/aswb/src/com/google/idea/blaze/android/sync/sdk/AndroidSdkFromProjectView.java
@@ -94,7 +94,7 @@
     return new AndroidSdkPlatform(androidSdk, androidSdkApiLevel);
   }
 
-  private static String getAvailableSdkPlatforms(Collection<Sdk> sdks) {
+  public static String getAvailableSdkPlatforms(Collection<Sdk> sdks) {
     List<String> names = Lists.newArrayList();
     for (Sdk sdk : sdks) {
       AndroidSdkAdditionalData additionalData = AndroidSdkUtils.getAndroidSdkAdditionalData(sdk);
diff --git a/aswb/src/com/google/idea/blaze/android/sync/sdk/SdkExperiment.java b/aswb/src/com/google/idea/blaze/android/sync/sdk/SdkExperiment.java
deleted file mode 100644
index 1ad1842..0000000
--- a/aswb/src/com/google/idea/blaze/android/sync/sdk/SdkExperiment.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.android.sync.sdk;
-
-import com.android.tools.idea.startup.AndroidStudioInitializer;
-
-/**
- * Wrapper class to keep track of "experiment" for later deletion.
- *
- * <p>The experiment is actually controlled by the JVM property, and we can't use a "normal"
- * experiment for that since the JVM property also affects upstream Android Studio.
- */
-public class SdkExperiment {
-  public static boolean useStandardSdkManager() {
-    return AndroidStudioInitializer.isAndroidSdkManagerEnabled();
-  }
-}
diff --git a/aswb/src/com/google/idea/blaze/android/sync/sdklegacy/AndroidSdkPlatformSyncer.java b/aswb/src/com/google/idea/blaze/android/sync/sdklegacy/AndroidSdkPlatformSyncer.java
deleted file mode 100644
index aa0fcd0..0000000
--- a/aswb/src/com/google/idea/blaze/android/sync/sdklegacy/AndroidSdkPlatformSyncer.java
+++ /dev/null
@@ -1,160 +0,0 @@
-/*
- * Copyright 2016 The Bazel Authors. All rights reserved.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.google.idea.blaze.android.sync.sdklegacy;
-
-import com.android.tools.idea.startup.AndroidStudioInitializer;
-import com.google.common.base.Joiner;
-import com.google.common.collect.ImmutableList;
-import com.google.common.collect.Iterables;
-import com.google.common.collect.Lists;
-import com.google.idea.blaze.android.projectview.AndroidSdkPlatformSection;
-import com.google.idea.blaze.android.settings.AswbGlobalSettings;
-import com.google.idea.blaze.android.sync.model.AndroidSdkPlatform;
-import com.google.idea.blaze.base.projectview.ProjectViewManager;
-import com.google.idea.blaze.base.projectview.ProjectViewSet;
-import com.google.idea.blaze.base.scope.BlazeContext;
-import com.google.idea.blaze.base.scope.output.IssueOutput;
-import com.google.idea.blaze.base.settings.Blaze;
-import com.google.idea.blaze.base.settings.Blaze.BuildSystem;
-import com.intellij.openapi.project.Project;
-import com.intellij.openapi.projectRoots.Sdk;
-import java.io.File;
-import java.util.List;
-import javax.annotation.Nullable;
-import org.jetbrains.android.sdk.AndroidPlatform;
-import org.jetbrains.android.sdk.AndroidSdkAdditionalData;
-import org.jetbrains.android.sdk.AndroidSdkUtils;
-
-/** Calculates AndroidSdkPlatform. */
-@Deprecated
-public class AndroidSdkPlatformSyncer {
-  @Nullable
-  public static AndroidSdkPlatform getAndroidSdkPlatform(Project project, BlazeContext context) {
-
-    final String localSdkLocation;
-    if (AndroidStudioInitializer.isAndroidSdkManagerEnabled()) {
-      Sdk sdk = Iterables.getFirst(AndroidSdkUtils.getAllAndroidSdks(), null);
-      if (sdk == null) {
-        IssueOutput.error(
-                "Error: No Android SDK configured. Please use the SDK manager to configure.")
-            .submit(context);
-        return null;
-      }
-      localSdkLocation = sdk.getHomePath();
-    } else {
-      localSdkLocation = AswbGlobalSettings.getInstance().getLocalSdkLocation();
-      if (localSdkLocation == null) {
-        IssueOutput.error(
-                "Error: No Android SDK synced yet."
-                    + (Blaze.defaultBuildSystem() == BuildSystem.Blaze
-                        ? " Please sync SDK following go/aswb-sdk."
-                        : ""))
-            .submit(context);
-        return null;
-      }
-    }
-
-    String androidSdkPlatform = null;
-    ProjectViewSet projectViewSet = ProjectViewManager.getInstance(project).getProjectViewSet();
-    if (projectViewSet != null) {
-      androidSdkPlatform = projectViewSet.getScalarValue(AndroidSdkPlatformSection.KEY);
-    }
-
-    // This is verified in the project view verification step, but double-check here
-    if (androidSdkPlatform == null) {
-      IssueOutput.error(
-              "No android_sdk_platform set. Please ensure this is set to a platform SDK directory.")
-          .submit(context);
-      return null;
-    }
-
-    String androidSdk =
-        BlazeAndroidSdk.getAndroidSdkLevelFromLocalChannel(localSdkLocation, androidSdkPlatform);
-
-    if (androidSdk == null) {
-      IssueOutput.error(
-              Joiner.on("\n")
-                  .join(
-                      "No such android_sdk_platform: " + androidSdkPlatform,
-                      "Available android_sdk_platforms are: "
-                          + getAvailableSdkPlatforms(localSdkLocation)))
-          .inFile(projectViewSet.getTopLevelProjectViewFile().projectViewFile)
-          .submit(context);
-      return null;
-    }
-
-    Sdk sdk = AndroidSdkUtils.findSuitableAndroidSdk(androidSdk);
-    if (sdk == null) {
-      ImmutableList.Builder<String> error =
-          ImmutableList.<String>builder()
-              .add(
-                  String.format(
-                      "Can't find a matching SDK "
-                          + "(was looking for '%s' in the '%s' platform directory).",
-                      androidSdk, androidSdkPlatform),
-                  "Available android_sdk_platforms are: "
-                      + getAvailableSdkPlatforms(localSdkLocation));
-      if (Blaze.defaultBuildSystem() == BuildSystem.Blaze) {
-        error.add(
-            "If you have no SDK, please sync your SDK by following go/aswb-sdk and try again. ",
-            "If you have done everything correctly, this can be due to an SDK sync manager bug.",
-            "To workaround, please delete ~/.AndroidStudioWithBlazeXX/system and restart");
-      }
-
-      IssueOutput.error(String.join("\n", error.build())).submit(context);
-      return null;
-    }
-
-    int androidSdkApiLevel = getAndroidSdkApiLevel(androidSdk);
-    return new AndroidSdkPlatform(androidSdk, androidSdkApiLevel);
-  }
-
-  private static String getAvailableSdkPlatforms(String localSdkDirectoryString) {
-    File localSdkDirectory = new File(localSdkDirectoryString);
-    if (localSdkDirectory.exists()) {
-      File platformDirectory = new File(localSdkDirectory, "platforms");
-      if (platformDirectory.exists()) {
-        File[] children = platformDirectory.listFiles();
-        if (children != null) {
-          List<String> names = Lists.newArrayList();
-          for (File child : children) {
-            if (child.isDirectory()) {
-              names.add('"' + child.getName() + '"');
-            }
-          }
-          return "{" + Joiner.on(", ").join(names) + "}";
-        }
-      }
-    }
-    return "<No platforms found>";
-  }
-
-  private static int getAndroidSdkApiLevel(String androidSdk) {
-    int androidSdkApiLevel = 1;
-    Sdk sdk = AndroidSdkUtils.findSuitableAndroidSdk(androidSdk);
-    if (sdk != null) {
-      AndroidSdkAdditionalData additionalData =
-          (AndroidSdkAdditionalData) sdk.getSdkAdditionalData();
-      if (additionalData != null) {
-        AndroidPlatform androidPlatform = additionalData.getAndroidPlatform();
-        if (androidPlatform != null) {
-          androidSdkApiLevel = androidPlatform.getApiLevel();
-        }
-      }
-    }
-    return androidSdkApiLevel;
-  }
-}
diff --git a/aswb/src/com/google/idea/blaze/android/sync/sdklegacy/BlazeAndroidSdk.java b/aswb/src/com/google/idea/blaze/android/sync/sdklegacy/BlazeAndroidSdk.java
deleted file mode 100644
index bee2b3b..0000000
--- a/aswb/src/com/google/idea/blaze/android/sync/sdklegacy/BlazeAndroidSdk.java
+++ /dev/null
@@ -1,97 +0,0 @@
-/*
- * Copyright 2016 The Bazel Authors. All rights reserved.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.google.idea.blaze.android.sync.sdklegacy;
-
-import com.android.SdkConstants;
-import com.android.sdklib.AndroidTargetHash;
-import com.android.sdklib.AndroidVersion;
-import com.android.sdklib.AndroidVersionHelper;
-import com.intellij.openapi.diagnostic.Logger;
-import java.io.File;
-import java.io.FileInputStream;
-import java.io.IOException;
-import java.io.InputStream;
-import java.util.Properties;
-import javax.annotation.Nullable;
-
-/** Utility methods for handling the android sdk. */
-@Deprecated
-final class BlazeAndroidSdk {
-  private static final Logger LOG = Logger.getInstance(BlazeAndroidSdk.class);
-
-  private BlazeAndroidSdk() {}
-
-  /** Reads the android sdk level from your local SDK directory. */
-  public static String getAndroidSdkLevelFromLocalChannel(
-      String localSdkLocation, String androidSdkPlatform) {
-    File androidSdkPlatformsDir =
-        new File(new File(new File(localSdkLocation), "platforms"), androidSdkPlatform);
-    File sourcePropertiesFile = new File(androidSdkPlatformsDir, SdkConstants.FN_SOURCE_PROP);
-    return getAndroidSdkLevelFromSourceProperties(sourcePropertiesFile);
-  }
-
-  @Nullable
-  public static String getAndroidSdkLevelFromSourceProperties(File sourcePropertiesFile) {
-    if (!sourcePropertiesFile.exists()) {
-      return null;
-    }
-
-    AndroidVersion androidVersion =
-        readAndroidVersionFromSourcePropertiesFile(sourcePropertiesFile);
-    if (androidVersion == null) {
-      LOG.warn("Could not read source.properties from: " + sourcePropertiesFile);
-      return null;
-    }
-    return AndroidTargetHash.getPlatformHashString(androidVersion);
-  }
-
-  @Nullable
-  private static AndroidVersion readAndroidVersionFromSourcePropertiesFile(
-      File sourcePropertiesFile) {
-    Properties props = parseProperties(sourcePropertiesFile);
-    if (props == null) {
-      return null;
-    }
-    try {
-      return AndroidVersionHelper.create(props);
-    } catch (AndroidVersion.AndroidVersionException e) {
-      return null;
-    }
-  }
-
-  /**
-   * Parses the given file as properties file if it exists. Returns null if the file does not exist,
-   * cannot be parsed or has no properties.
-   */
-  @Nullable
-  private static Properties parseProperties(File propsFile) {
-    if (!propsFile.exists()) {
-      return null;
-    }
-    try (InputStream fis = new FileInputStream(propsFile)) {
-      Properties props = new Properties();
-      props.load(fis);
-
-      // To be valid, there must be at least one property in it.
-      if (props.size() > 0) {
-        return props;
-      }
-    } catch (IOException e) {
-      // Ignore
-    }
-    return null;
-  }
-}
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 1f16d2d..c839293 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
@@ -19,6 +19,8 @@
 
 import com.google.common.collect.ImmutableSet;
 import com.google.common.collect.Lists;
+import com.google.idea.blaze.android.projectview.GeneratedAndroidResourcesSection;
+import com.google.idea.blaze.android.projectview.GenfilesPath;
 import com.google.idea.blaze.android.sync.BlazeAndroidJavaSyncAugmenter;
 import com.google.idea.blaze.android.sync.model.AndroidResourceModule;
 import com.google.idea.blaze.android.sync.model.BlazeAndroidImportResult;
@@ -34,6 +36,7 @@
 import com.google.idea.blaze.base.ideinfo.TargetKey;
 import com.google.idea.blaze.base.ideinfo.TargetMap;
 import com.google.idea.blaze.base.ideinfo.TargetMapBuilder;
+import com.google.idea.blaze.base.io.FileAttributeProvider;
 import com.google.idea.blaze.base.model.primitives.Label;
 import com.google.idea.blaze.base.model.primitives.LanguageClass;
 import com.google.idea.blaze.base.model.primitives.WorkspacePath;
@@ -52,6 +55,7 @@
 import com.google.idea.blaze.base.settings.BlazeImportSettingsManager;
 import com.google.idea.blaze.base.sync.projectview.ImportRoots;
 import com.google.idea.blaze.base.sync.projectview.WorkspaceLanguageSettings;
+import com.google.idea.blaze.base.sync.workspace.ArtifactLocationDecoder;
 import com.google.idea.blaze.java.sync.model.BlazeJarLibrary;
 import com.google.idea.common.experiments.ExperimentService;
 import com.google.idea.common.experiments.MockExperimentService;
@@ -72,6 +76,10 @@
   private static final String FAKE_GEN_ROOT_EXECUTION_PATH_FRAGMENT =
       "blaze-out/gcc-4.X.Y-crosstool-v17-hybrid-grtev3-k8-fastbuild/bin";
 
+  private static final ArtifactLocationDecoder FAKE_ARTIFACT_DECODER =
+      (ArtifactLocationDecoder)
+          artifactLocation -> new File("/", artifactLocation.getRelativePath());
+
   private static final BlazeImportSettings DUMMY_IMPORT_SETTINGS =
       new BlazeImportSettings("", "", "", "", "", BuildSystem.Blaze);
 
@@ -91,6 +99,9 @@
         BlazeImportSettingsManager.class, new BlazeImportSettingsManager(project));
     BlazeImportSettingsManager.getInstance(getProject()).setImportSettings(DUMMY_IMPORT_SETTINGS);
 
+    MockFileAttributeProvider mockFileAttributeProvider = new MockFileAttributeProvider();
+    applicationServices.register(FileAttributeProvider.class, mockFileAttributeProvider);
+
     context = new BlazeContext();
     context.addOutputSink(IssueOutput.class, errorCollector);
   }
@@ -102,7 +113,12 @@
 
     BlazeAndroidWorkspaceImporter workspaceImporter =
         new BlazeAndroidWorkspaceImporter(
-            project, context, workspaceRoot, projectViewSet, targetMapBuilder.build());
+            project,
+            context,
+            workspaceRoot,
+            projectViewSet,
+            targetMapBuilder.build(),
+            FAKE_ARTIFACT_DECODER);
 
     return workspaceImporter.importWorkspace();
   }
@@ -364,9 +380,11 @@
     WorkspaceLanguageSettings workspaceLanguageSettings =
         new WorkspaceLanguageSettings(
             WorkspaceType.ANDROID, ImmutableSet.of(LanguageClass.ANDROID, LanguageClass.JAVA));
+    ProjectViewSet projectViewSet = ProjectViewSet.builder().add(projectView).build();
     for (TargetIdeInfo target : targetMap.targets()) {
       if (importRoots.importAsSource(target.key.label)) {
-        syncAugmenter.addJarsForSourceTarget(workspaceLanguageSettings, target, jars, genJars);
+        syncAugmenter.addJarsForSourceTarget(
+            workspaceLanguageSettings, projectViewSet, target, jars, genJars);
       }
     }
 
@@ -416,9 +434,11 @@
     WorkspaceLanguageSettings workspaceLanguageSettings =
         new WorkspaceLanguageSettings(
             WorkspaceType.ANDROID, ImmutableSet.of(LanguageClass.ANDROID, LanguageClass.JAVA));
+    ProjectViewSet projectViewSet = ProjectViewSet.builder().add(projectView).build();
     for (TargetIdeInfo target : targetMap.targets()) {
       if (importRoots.importAsSource(target.key.label)) {
-        syncAugmenter.addJarsForSourceTarget(workspaceLanguageSettings, target, jars, genJars);
+        syncAugmenter.addJarsForSourceTarget(
+            workspaceLanguageSettings, projectViewSet, target, jars, genJars);
       }
     }
     assertThat(
@@ -599,7 +619,182 @@
                     .build());
 
     importWorkspace(workspaceRoot, targetMapBuilder, projectView);
-    errorCollector.assertIssueContaining("Dropping generated resource");
+    errorCollector.assertIssueContaining("Dropping 1 generated resource");
+  }
+
+  @Test
+  public void testMixingGeneratedAndNonGeneratedSourcesWhitelisted() {
+    ProjectView projectView =
+        ProjectView.builder()
+            .add(
+                ListSection.builder(DirectorySection.KEY)
+                    .add(DirectoryEntry.include(new WorkspacePath("java/example"))))
+            .add(
+                ListSection.builder(GeneratedAndroidResourcesSection.KEY)
+                    .add(new GenfilesPath("java/example/res")))
+            .build();
+
+    TargetMapBuilder targetMapBuilder =
+        TargetMapBuilder.builder()
+            .addTarget(
+                TargetIdeInfo.builder()
+                    .setLabel("//java/example:lib")
+                    .setBuildFile(source("java/example/BUILD"))
+                    .setKind("android_library")
+                    .setAndroidInfo(
+                        AndroidIdeInfo.builder()
+                            .setManifestFile(source("java/example/AndroidManifest.xml"))
+                            .addResource(source("java/example/res"))
+                            .addResource(gen("java/example/res"))
+                            .setGenerateResourceClass(true)
+                            .setResourceJavaPackage("com.google.android.example"))
+                    .build());
+
+    BlazeAndroidImportResult result = importWorkspace(workspaceRoot, targetMapBuilder, projectView);
+    errorCollector.assertNoIssues();
+    assertThat(result.androidResourceModules)
+        .containsExactly(
+            AndroidResourceModule.builder(TargetKey.forPlainTarget(new Label("//java/example:lib")))
+                .addResourceAndTransitiveResource(source("java/example/res"))
+                .addResourceAndTransitiveResource(gen("java/example/res"))
+                .build());
+  }
+
+  @Test
+  public void testMixingGeneratedAndNonGeneratedSourcesPartlyWhitelisted() {
+    ProjectView projectView =
+        ProjectView.builder()
+            .add(
+                ListSection.builder(DirectorySection.KEY)
+                    .add(DirectoryEntry.include(new WorkspacePath("java/example")))
+                    .add(DirectoryEntry.include(new WorkspacePath("java/example2")))
+                    .add(DirectoryEntry.include(new WorkspacePath("java/uninterestingdir"))))
+            .add(
+                ListSection.builder(GeneratedAndroidResourcesSection.KEY)
+                    .add(new GenfilesPath("java/example/res"))
+                    .add(new GenfilesPath("unused/whitelisted/path/res")))
+            .build();
+
+    TargetMapBuilder targetMapBuilder =
+        TargetMapBuilder.builder()
+            .addTarget(
+                TargetIdeInfo.builder()
+                    .setLabel("//java/example:lib")
+                    .setBuildFile(source("java/example/BUILD"))
+                    .setKind("android_library")
+                    .setAndroidInfo(
+                        AndroidIdeInfo.builder()
+                            .setManifestFile(source("java/example/AndroidManifest.xml"))
+                            .addResource(source("java/example/res"))
+                            .addResource(gen("java/example/res"))
+                            .setGenerateResourceClass(true)
+                            .setResourceJavaPackage("com.google.android.example"))
+                    .build())
+            .addTarget(
+                TargetIdeInfo.builder()
+                    .setLabel("//java/example2:lib")
+                    .setBuildFile(source("java/example2/BUILD"))
+                    .setKind("android_library")
+                    .setAndroidInfo(
+                        AndroidIdeInfo.builder()
+                            .setManifestFile(source("java/example2/AndroidManifest.xml"))
+                            .addResource(source("java/example2/res"))
+                            .addResource(gen("java/example2/res"))
+                            .setGenerateResourceClass(true)
+                            .setResourceJavaPackage("com.google.android.example2"))
+                    .build())
+            .addTarget(
+                TargetIdeInfo.builder()
+                    .setLabel("//java/uninterestingdir:lib")
+                    .setBuildFile(source("java/uninterestingdir/BUILD"))
+                    .setKind("android_library")
+                    .setAndroidInfo(
+                        AndroidIdeInfo.builder()
+                            .setManifestFile(source("java/uninterestingdir/AndroidManifest.xml"))
+                            .addResource(source("java/uninterestingdir/res"))
+                            .addResource(gen("java/uninterestingdir/res"))
+                            .setGenerateResourceClass(true)
+                            .setResourceJavaPackage("com.google.android.uninterestingdir"))
+                    .build());
+
+    importWorkspace(workspaceRoot, targetMapBuilder, projectView);
+    errorCollector.assertIssues(
+        "Dropping 1 generated resource directories.\n"
+            + "R classes will not contain resources from these directories.\n"
+            + "Double-click to add to project view if needed to resolve references.",
+        "Dropping generated resource directory "
+            + String.format("'%s/java/example2/res'", FAKE_GEN_ROOT_EXECUTION_PATH_FRAGMENT)
+            + " w/ 2 subdirs",
+        "1 unused entries in project view section \"generated_android_resource_directories\":\n"
+            + "unused/whitelisted/path/res");
+  }
+
+  @Test
+  public void testMixingGeneratedAndNonGeneratedSourcesNoInterestingDirectories() {
+    ProjectView projectView =
+        ProjectView.builder()
+            .add(
+                ListSection.builder(DirectorySection.KEY)
+                    .add(DirectoryEntry.include(new WorkspacePath("java/uninterestingdir"))))
+            .build();
+
+    TargetMapBuilder targetMapBuilder =
+        TargetMapBuilder.builder()
+            .addTarget(
+                TargetIdeInfo.builder()
+                    .setLabel("//java/uninterestingdir:lib")
+                    .setBuildFile(source("java/uninterestingdir/BUILD"))
+                    .setKind("android_library")
+                    .setAndroidInfo(
+                        AndroidIdeInfo.builder()
+                            .setManifestFile(source("java/uninterestingdir/AndroidManifest.xml"))
+                            .addResource(source("java/uninterestingdir/res"))
+                            .addResource(gen("java/uninterestingdir/res"))
+                            .setGenerateResourceClass(true)
+                            .setResourceJavaPackage("com.google.android.uninterestingdir"))
+                    .build());
+
+    BlazeAndroidImportResult result = importWorkspace(workspaceRoot, targetMapBuilder, projectView);
+    errorCollector.assertNoIssues();
+    assertThat(result.androidResourceModules)
+        .containsExactly(
+            AndroidResourceModule.builder(
+                    TargetKey.forPlainTarget(new Label("//java/uninterestingdir:lib")))
+                .addResourceAndTransitiveResource(source("java/uninterestingdir/res"))
+                .build());
+  }
+
+  /**
+   * Mock provider to satisfy directory listing queries from {@link
+   * com.google.idea.blaze.android.sync.importer.problems.GeneratedResourceClassifier}.
+   */
+  private static class MockFileAttributeProvider extends FileAttributeProvider {
+
+    // Return a few non-translation directories so that directories are considered interesting,
+    // or return only-translation directories so that it's considered uninteresting.
+    @Override
+    public File[] listFiles(File directory) {
+      File interestingResDir1 = FAKE_ARTIFACT_DECODER.decode(gen("java/example/res"));
+      if (directory.equals(interestingResDir1)) {
+        return new File[] {
+          new File("java/example/res/raw"), new File("java/example/res/values-es"),
+        };
+      }
+      File interestingResDir2 = FAKE_ARTIFACT_DECODER.decode(gen("java/example2/res"));
+      if (directory.equals(interestingResDir2)) {
+        return new File[] {
+          new File("java/example2/res/layout"), new File("java/example2/res/values-ar"),
+        };
+      }
+      File uninterestingResDir = FAKE_ARTIFACT_DECODER.decode(gen("java/uninterestingdir/res"));
+      if (directory.equals(uninterestingResDir)) {
+        return new File[] {
+          new File("java/uninterestingdir/res/values-ar"),
+          new File("java/uninterestingdir/res/values-es"),
+        };
+      }
+      return new File[0];
+    }
   }
 
   private ArtifactLocation source(String relativePath) {
diff --git a/aswb/tests/unittests/com/google/idea/blaze/android/sync/importer/problems/GeneratedResourceClassifierTest.java b/aswb/tests/unittests/com/google/idea/blaze/android/sync/importer/problems/GeneratedResourceClassifierTest.java
new file mode 100644
index 0000000..ab9d124
--- /dev/null
+++ b/aswb/tests/unittests/com/google/idea/blaze/android/sync/importer/problems/GeneratedResourceClassifierTest.java
@@ -0,0 +1,116 @@
+/*
+ * Copyright 2016 The Bazel Authors. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.idea.blaze.android.sync.importer.problems;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.google.idea.blaze.base.BlazeTestCase;
+import java.io.File;
+import java.util.Arrays;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** Tests for {@link GeneratedResourceClassifier}. */
+@RunWith(JUnit4.class)
+public class GeneratedResourceClassifierTest extends BlazeTestCase {
+
+  @Test
+  public void rankEmpty() {
+    assertThat(GeneratedResourceClassifier.mayHaveNonStringTranslations(files())).isFalse();
+  }
+
+  @Test
+  public void rankOneValues() {
+    assertThat(GeneratedResourceClassifier.mayHaveNonStringTranslations(files("foo/res/values")))
+        .isTrue();
+  }
+
+  @Test
+  public void rankOneTranslation() {
+    assertThat(GeneratedResourceClassifier.mayHaveNonStringTranslations(files("foo/res/values-af")))
+        .isFalse();
+  }
+
+  @Test
+  public void rankOneOtherType() {
+    assertThat(GeneratedResourceClassifier.mayHaveNonStringTranslations(files("foo/res/raw")))
+        .isTrue();
+  }
+
+  @Test
+  public void onlyTranslations() {
+    assertThat(
+            GeneratedResourceClassifier.mayHaveNonStringTranslations(
+                files("foo/res/values-af", "foo/res/values-ar", "foo/res/values-b+sr+Latn")))
+        .isFalse();
+  }
+
+  @Test
+  public void onlyTranslationsWithOther() {
+    assertThat(
+            GeneratedResourceClassifier.mayHaveNonStringTranslations(
+                files(
+                    "foo/res/values-af",
+                    "foo/res/values-fr-rCA",
+                    "foo/res/values-b+sr+Latn",
+                    "foo/res/values-af-sw600dp",
+                    "foo/res/values-fr-rCA-sw600dp",
+                    "foo/res/values-b+sr+Latn-sw600dp")))
+        .isFalse();
+  }
+
+  @Test
+  public void mixedWithTranslationsAndDefaultValues() {
+    assertThat(
+            GeneratedResourceClassifier.mayHaveNonStringTranslations(
+                files("foo/res/values", "foo/res/values-af", "foo/res/values-fr-rCA")))
+        .isTrue();
+  }
+
+  @Test
+  public void mixedWithTranslationsDrawableXml() {
+    assertThat(
+            GeneratedResourceClassifier.mayHaveNonStringTranslations(
+                files(
+                    "foo/res/drawable-xxhdpi",
+                    "foo/res/values-af",
+                    "foo/res/values-fr-rCA",
+                    "foo/res/xml")))
+        .isTrue();
+  }
+
+  @Test
+  public void mixedWithIncorrectConfig() {
+    assertThat(
+            GeneratedResourceClassifier.mayHaveNonStringTranslations(
+                files(
+                    "foo/res/values-notaqualifier", "foo/res/values-af", "foo/res/values-fr-rCA")))
+        .isTrue();
+  }
+
+  @Test
+  public void mixedWithIncorrectFolder() {
+    assertThat(
+            GeneratedResourceClassifier.mayHaveNonStringTranslations(
+                files("foo/res/php_scripts", "foo/res/values-af", "foo/res/values-fr-rCA")))
+        .isTrue();
+  }
+
+  private static File[] files(String... paths) {
+    return Arrays.stream(paths).map(File::new).toArray(File[]::new);
+  }
+}
diff --git a/base/BUILD b/base/BUILD
index 2921507..6902fd5 100644
--- a/base/BUILD
+++ b/base/BUILD
@@ -8,6 +8,7 @@
     deps = [
         "//common/binaryhelper",
         "//common/experiments",
+        "//common/formatter",
         "//intellij_platform_sdk:plugin_api",
         "//proto_deps",
         "@jsr305_annotations//jar",
diff --git a/base/src/META-INF/blaze-base.xml b/base/src/META-INF/blaze-base.xml
index ba5bbc3..1e12599 100644
--- a/base/src/META-INF/blaze-base.xml
+++ b/base/src/META-INF/blaze-base.xml
@@ -161,6 +161,7 @@
                         serviceImplementation="com.google.idea.blaze.base.help.BlazeHelpHandlerImpl"/>
 
     <additionalTextAttributes scheme="Default" file="base/resources/colorSchemes/BuildDefault.xml"/>
+    <typedHandler implementation="com.google.idea.blaze.base.lang.buildfile.completion.BuildCompletionAutoPopupHandler"/>
   </extensions>
 
   <extensions defaultExtensionNs="com.intellij">
@@ -194,12 +195,14 @@
     <enterHandlerDelegate implementation="com.google.idea.blaze.base.lang.buildfile.editor.BuildEnterHandler" order="after EnterBetweenBracesHandler"/>
     <completion.contributor language="BUILD" implementationClass="com.google.idea.blaze.base.lang.buildfile.completion.ParameterCompletionContributor"/>
     <completion.contributor language="BUILD" implementationClass="com.google.idea.blaze.base.lang.buildfile.completion.BuiltInFunctionCompletionContributor"/>
+    <completion.contributor language="BUILD" implementationClass="com.google.idea.blaze.base.lang.buildfile.completion.BuiltInSymbolCompletionContributor"/>
     <completion.contributor language="BUILD" implementationClass="com.google.idea.blaze.base.lang.buildfile.completion.BuiltInFunctionAttributeCompletionContributor"/>
     <completion.contributor language="BUILD" implementationClass="com.google.idea.blaze.base.lang.buildfile.completion.ArgumentCompletionContributor"/>
     <langCodeStyleSettingsProvider implementation="com.google.idea.blaze.base.lang.buildfile.formatting.BuildLanguageCodeStyleSettingsProvider"/>
     <codeStyleSettingsProvider implementation="com.google.idea.blaze.base.lang.buildfile.formatting.BuildCodeStyleSettingsProvider"/>
     <editor.backspaceModeOverride language="BUILD" implementationClass="com.intellij.codeInsight.editorActions.SmartBackspaceDisabler"/>
     <filetype.stubBuilder filetype="BUILD" implementationClass="com.google.idea.blaze.base.lang.buildfile.stubs.BuildFileStubBuilder"/>
+    <editorNotificationProvider implementation="com.google.idea.blaze.base.lang.AdditionalLanguagesHelper"/>
   </extensions>
 
   <extensions defaultExtensionNs="com.intellij.lang">
@@ -233,6 +236,13 @@
     </component>
   </application-components>
 
+  <project-components>
+    <component>
+      <implementation-class>com.google.idea.blaze.base.buildmodifier.BuildifierFormatter</implementation-class>
+      <loadForDefaultProject/>
+    </component>
+  </project-components>
+
   <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"/>
diff --git a/base/src/com/google/idea/blaze/base/buildmodifier/BuildifierDelegatingCodeStyleManager.java b/base/src/com/google/idea/blaze/base/buildmodifier/BuildifierDelegatingCodeStyleManager.java
new file mode 100644
index 0000000..cf4574a
--- /dev/null
+++ b/base/src/com/google/idea/blaze/base/buildmodifier/BuildifierDelegatingCodeStyleManager.java
@@ -0,0 +1,126 @@
+/*
+ * 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.buildmodifier;
+
+import static java.util.Comparator.comparing;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+import com.google.idea.blaze.base.lang.buildfile.psi.BuildFile;
+import com.google.idea.blaze.base.lang.buildfile.psi.BuildFile.BlazeFileType;
+import com.google.idea.common.formatter.DelegatingCodeStyleManager;
+import com.intellij.openapi.application.ApplicationManager;
+import com.intellij.openapi.command.WriteCommandAction;
+import com.intellij.openapi.editor.Document;
+import com.intellij.openapi.util.TextRange;
+import com.intellij.psi.PsiDocumentManager;
+import com.intellij.psi.PsiFile;
+import com.intellij.psi.codeStyle.CodeStyleManager;
+import com.intellij.psi.impl.CheckUtil;
+import com.intellij.util.IncorrectOperationException;
+import java.util.Collection;
+import java.util.Map;
+import java.util.TreeMap;
+
+/**
+ * A {@link CodeStyleManager} implementation which runs buildifier on BUILD files, and otherwise
+ * delegates to the existing formatter.
+ */
+public class BuildifierDelegatingCodeStyleManager extends DelegatingCodeStyleManager {
+
+  public BuildifierDelegatingCodeStyleManager(CodeStyleManager original) {
+    super(original);
+  }
+
+  @Override
+  public void reformatText(PsiFile file, int startOffset, int endOffset)
+      throws IncorrectOperationException {
+    if (overrideFormatterForFile(file)) {
+      formatInternal(file, ImmutableList.of(new TextRange(startOffset, endOffset)));
+    } else {
+      super.reformatText(file, startOffset, endOffset);
+    }
+  }
+
+  @Override
+  public void reformatText(PsiFile file, Collection<TextRange> ranges)
+      throws IncorrectOperationException {
+    if (overrideFormatterForFile(file)) {
+      formatInternal(file, ranges);
+    } else {
+      super.reformatText(file, ranges);
+    }
+  }
+
+  @Override
+  public void reformatTextWithContext(PsiFile file, Collection<TextRange> ranges)
+      throws IncorrectOperationException {
+    if (overrideFormatterForFile(file)) {
+      formatInternal(file, ranges);
+    } else {
+      super.reformatTextWithContext(file, ranges);
+    }
+  }
+
+  private static boolean overrideFormatterForFile(PsiFile file) {
+    // don't format skylark extensions
+    return file instanceof BuildFile
+        && ((BuildFile) file).getBlazeFileType() == BlazeFileType.BuildPackage;
+  }
+
+  private void formatInternal(PsiFile file, Collection<TextRange> ranges) {
+    ApplicationManager.getApplication().assertWriteAccessAllowed();
+    PsiDocumentManager.getInstance(getProject()).commitAllDocuments();
+    CheckUtil.checkWritable(file);
+
+    Document document = PsiDocumentManager.getInstance(getProject()).getDocument(file);
+    if (document == null) {
+      return;
+    }
+    Map<TextRange, String> replacements = getFormatReplacements(document.getText(), ranges);
+    TreeMap<TextRange, String> sortedReplacements =
+        new TreeMap<>(comparing(TextRange::getStartOffset));
+    sortedReplacements.putAll(replacements);
+    performReplacements(document, sortedReplacements);
+  }
+
+  private static Map<TextRange, String> getFormatReplacements(
+      String text, Collection<TextRange> ranges) {
+    ImmutableMap.Builder<TextRange, String> output = ImmutableMap.builder();
+    for (TextRange range : ranges) {
+      String result = BuildFileFormatter.formatText(range.substring(text));
+      if (result == null) {
+        return ImmutableMap.of();
+      }
+      output.put(range, result);
+    }
+    return output.build();
+  }
+
+  private void performReplacements(
+      final Document document, final Map<TextRange, String> reverseSortedReplacements) {
+    WriteCommandAction.runWriteCommandAction(
+        getProject(),
+        () -> {
+          for (Map.Entry<TextRange, String> replacement : reverseSortedReplacements.entrySet()) {
+            TextRange range = replacement.getKey();
+            document.replaceString(
+                range.getStartOffset(), range.getEndOffset(), replacement.getValue());
+          }
+          PsiDocumentManager.getInstance(getProject()).commitDocument(document);
+        });
+  }
+}
diff --git a/base/src/com/google/idea/blaze/base/buildmodifier/BuildifierFormatter.java b/base/src/com/google/idea/blaze/base/buildmodifier/BuildifierFormatter.java
new file mode 100644
index 0000000..ce786b3
--- /dev/null
+++ b/base/src/com/google/idea/blaze/base/buildmodifier/BuildifierFormatter.java
@@ -0,0 +1,33 @@
+/*
+ * 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.buildmodifier;
+
+import com.google.idea.common.formatter.FormatterInstaller;
+import com.intellij.openapi.components.AbstractProjectComponent;
+import com.intellij.openapi.project.Project;
+
+/** Integrates buildifier with IntelliJ's formatter. */
+public class BuildifierFormatter extends AbstractProjectComponent {
+
+  protected BuildifierFormatter(Project project) {
+    super(project);
+  }
+
+  @Override
+  public void projectOpened() {
+    FormatterInstaller.replaceFormatter(myProject, BuildifierDelegatingCodeStyleManager::new);
+  }
+}
diff --git a/base/src/com/google/idea/blaze/base/buildmodifier/FileSaveHandler.java b/base/src/com/google/idea/blaze/base/buildmodifier/FileSaveHandler.java
index df5e61a..daeae84 100644
--- a/base/src/com/google/idea/blaze/base/buildmodifier/FileSaveHandler.java
+++ b/base/src/com/google/idea/blaze/base/buildmodifier/FileSaveHandler.java
@@ -16,6 +16,7 @@
 package com.google.idea.blaze.base.buildmodifier;
 
 import com.google.idea.blaze.base.bazel.BuildSystemProvider;
+import com.google.idea.blaze.base.settings.BlazeUserSettings;
 import com.intellij.openapi.application.ApplicationManager;
 import com.intellij.openapi.command.CommandProcessor;
 import com.intellij.openapi.editor.Document;
@@ -29,7 +30,7 @@
 
   @Override
   public void beforeDocumentSaving(final Document document) {
-    if (!document.isWritable()) {
+    if (!BlazeUserSettings.getInstance().getFormatBuildFilesOnSave() || !document.isWritable()) {
       return;
     }
     FileDocumentManager fileDocumentManager = FileDocumentManager.getInstance();
diff --git a/base/src/com/google/idea/blaze/base/io/FileAttributeProvider.java b/base/src/com/google/idea/blaze/base/io/FileAttributeProvider.java
index 69f8465..aeb0f24 100644
--- a/base/src/com/google/idea/blaze/base/io/FileAttributeProvider.java
+++ b/base/src/com/google/idea/blaze/base/io/FileAttributeProvider.java
@@ -44,4 +44,8 @@
   public long getFileSize(File file) {
     return file.length();
   }
+
+  public File[] listFiles(File file) {
+    return file.listFiles();
+  }
 }
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 ef4337a..2ebd868 100644
--- a/base/src/com/google/idea/blaze/base/issueparser/BlazeIssueParser.java
+++ b/base/src/com/google/idea/blaze/base/issueparser/BlazeIssueParser.java
@@ -17,27 +17,25 @@
 
 import static com.google.common.base.Preconditions.checkState;
 
-import com.google.common.base.Predicate;
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.Lists;
+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;
-import com.google.idea.blaze.base.projectview.ProjectViewManager;
 import com.google.idea.blaze.base.projectview.ProjectViewSet;
-import com.google.idea.blaze.base.projectview.section.ListSection;
 import com.google.idea.blaze.base.projectview.section.Section;
 import com.google.idea.blaze.base.projectview.section.SectionKey;
 import com.google.idea.blaze.base.projectview.section.sections.TargetSection;
 import com.google.idea.blaze.base.scope.output.IssueOutput;
-import com.intellij.openapi.project.Project;
 import java.io.File;
 import java.util.ArrayList;
 import java.util.List;
+import java.util.function.Predicate;
 import java.util.regex.Matcher;
 import java.util.regex.Pattern;
-import org.jetbrains.annotations.NotNull;
-import org.jetbrains.annotations.Nullable;
+import javax.annotation.Nullable;
+
 
 /** Parses blaze output for compile errors. */
 public class BlazeIssueParser {
@@ -69,27 +67,27 @@
     }
   }
 
-  interface Parser {
-    @NotNull
-    ParseResult parse(@NotNull String currentLine, @NotNull List<String> previousLines);
+  /** Used by BlazeIssueParser. Generally implemented by subclassing SingleLineParser */
+  public interface Parser {
+    ParseResult parse(String currentLine, List<String> previousLines);
   }
 
-  abstract static class SingleLineParser implements Parser {
-    @NotNull Pattern pattern;
+  /** Base for a Parser that consumes a single contextless line at a time, matched via regex */
+  public abstract static class SingleLineParser implements Parser {
+    Pattern pattern;
 
-    SingleLineParser(@NotNull String regex) {
+    public SingleLineParser(String regex) {
       pattern = Pattern.compile(regex);
     }
 
     @Override
-    public ParseResult parse(
-        @NotNull String currentLine, @NotNull List<String> multilineMatchResult) {
+    public ParseResult parse(String currentLine, List<String> multilineMatchResult) {
       checkState(
           multilineMatchResult.isEmpty(), "SingleLineParser recieved multiple lines of input");
       return parse(currentLine);
     }
 
-    ParseResult parse(@NotNull String line) {
+    ParseResult parse(String line) {
       Matcher matcher = pattern.matcher(line);
       if (matcher.find()) {
         return ParseResult.output(createIssue(matcher));
@@ -98,36 +96,61 @@
     }
 
     @Nullable
-    protected abstract IssueOutput createIssue(@NotNull Matcher matcher);
+    protected abstract IssueOutput createIssue(Matcher matcher);
+  }
+
+  @Nullable
+  public static File fileFromAbsolutePath(String absolutePath) {
+    return new File(absolutePath);
+  }
+
+  @Nullable
+  public static File fileFromRelativePath(WorkspaceRoot workspaceRoot, String relativePath) {
+    try {
+      final WorkspacePath workspacePath = new WorkspacePath(relativePath);
+      return workspaceRoot.fileForPath(workspacePath);
+    } catch (IllegalArgumentException e) {
+      // Ignore -- malformed error message
+      return null;
+    }
+  }
+
+  /** Returns the file referenced by the target */
+  @Nullable
+  public static File fileFromTarget(WorkspaceRoot workspaceRoot, String targetString) {
+    Label label = Label.createIfValid(targetString);
+    if (label == null) {
+      return null;
+    }
+    try {
+      final WorkspacePath combined =
+          new WorkspacePath(label.blazePackage(), label.targetName().toString());
+      return workspaceRoot.fileForPath(combined);
+    } catch (IllegalArgumentException e) {
+      return null;
+    }
+  }
+
+  /** Falls back to returning -1 if no integer can be parsed. */
+  public static int parseOptionalInt(String intString) {
+    try {
+      return Integer.parseInt(intString);
+    } catch (NumberFormatException e) {
+      return -1;
+    }
   }
 
   static class CompileParser extends SingleLineParser {
-    @NotNull private final WorkspaceRoot workspaceRoot;
+    private final WorkspaceRoot workspaceRoot;
 
-    public CompileParser(@NotNull WorkspaceRoot workspaceRoot) {
-      super("(.*?):([0-9]+):([0-9]+:)? (error|warning): (.*)");
+    CompileParser(WorkspaceRoot workspaceRoot) {
+      super("^([^/].*?):([0-9]+):(?:([0-9]+):)? (error|warning): (.*)$");
       this.workspaceRoot = workspaceRoot;
     }
 
     @Override
-    protected IssueOutput createIssue(@NotNull Matcher matcher) {
-      final File file;
-      try {
-        String fileName = matcher.group(1);
-        final WorkspacePath workspacePath;
-        if (fileName.startsWith("//depot/google3/")) {
-          workspacePath = new WorkspacePath(fileName.substring("//depot/google3/".length()));
-        } else if (fileName.startsWith("/")) {
-          workspacePath = workspaceRoot.workspacePathFor(new File(fileName));
-        } else {
-          workspacePath = new WorkspacePath(fileName);
-        }
-        file = workspaceRoot.fileForPath(workspacePath);
-      } catch (IllegalArgumentException e) {
-        // Ignore -- malformed error message
-        return null;
-      }
-
+    protected IssueOutput createIssue(Matcher matcher) {
+      final File file = fileFromRelativePath(workspaceRoot, matcher.group(1));
       IssueOutput.Category type =
           matcher.group(4).equals("error")
               ? IssueOutput.Category.ERROR
@@ -135,7 +158,7 @@
       return IssueOutput.issue(type, matcher.group(5))
           .inFile(file)
           .onLine(Integer.parseInt(matcher.group(2)))
-          .inColumn(parseOptionalInt(matcher.group(4)))
+          .inColumn(parseOptionalInt(matcher.group(3)))
           .build();
     }
   }
@@ -145,9 +168,8 @@
         Pattern.compile(
             "(ERROR): (.*?):([0-9]+):([0-9]+): (Traceback \\(most recent call last\\):)");
 
-    @NotNull
     @Override
-    public ParseResult parse(@NotNull String currentLine, @NotNull List<String> previousLines) {
+    public ParseResult parse(String currentLine, List<String> previousLines) {
       if (previousLines.isEmpty()) {
         if (PATTERN.matcher(currentLine).find()) {
           return ParseResult.needsMoreInput();
@@ -179,36 +201,43 @@
 
   static class BuildParser extends SingleLineParser {
     BuildParser() {
-      super("(ERROR): (.*?):([0-9]+):([0-9]+): (.*)");
+      super("^ERROR: (/.*?BUILD):([0-9]+):([0-9]+): (.*)$");
     }
 
     @Override
-    protected IssueOutput createIssue(@NotNull Matcher matcher) {
-      return IssueOutput.error(matcher.group(5))
-          .inFile(new File(matcher.group(2)))
-          .onLine(Integer.parseInt(matcher.group(3)))
-          .inColumn(parseOptionalInt(matcher.group(4)))
+    protected IssueOutput createIssue(Matcher matcher) {
+      File file = fileFromAbsolutePath(matcher.group(1));
+      return IssueOutput.error(matcher.group(4))
+          .inFile(file)
+          .onLine(Integer.parseInt(matcher.group(2)))
+          .inColumn(parseOptionalInt(matcher.group(3)))
           .build();
     }
   }
 
-  /** Falls back to returning -1 if no integer can be parsed. */
-  private static int parseOptionalInt(String intString) {
-    try {
-      return Integer.parseInt(intString);
-    } catch (NumberFormatException e) {
-      return -1;
-    }
-  }
-
   static class LinelessBuildParser extends SingleLineParser {
     LinelessBuildParser() {
-      super("(ERROR): (.*?):char offsets [0-9]+--[0-9]+: (.*)");
+      super("^ERROR: (.*?):char offsets [0-9]+--[0-9]+: (.*)$");
     }
 
     @Override
-    protected IssueOutput createIssue(@NotNull Matcher matcher) {
-      return IssueOutput.error(matcher.group(3)).inFile(new File(matcher.group(2))).build();
+    protected IssueOutput createIssue(Matcher matcher) {
+      return IssueOutput.error(matcher.group(2)).inFile(new File(matcher.group(1))).build();
+    }
+  }
+
+  static class FileNotFoundBuildParser extends SingleLineParser {
+    private final WorkspaceRoot workspaceRoot;
+
+    FileNotFoundBuildParser(WorkspaceRoot workspaceRoot) {
+      super("^ERROR: .*? Unable to load file '(.*?)': (.*)$");
+      this.workspaceRoot = workspaceRoot;
+    }
+
+    @Override
+    protected IssueOutput createIssue(Matcher matcher) {
+      File file = fileFromTarget(workspaceRoot, matcher.group(1));
+      return IssueOutput.error(matcher.group(2)).inFile(file).build();
     }
   }
 
@@ -222,7 +251,7 @@
     }
 
     @Override
-    protected IssueOutput createIssue(@NotNull Matcher matcher) {
+    protected IssueOutput createIssue(Matcher matcher) {
       File file = null;
       if (projectViewSet != null) {
         String targetString = matcher.group(1);
@@ -231,12 +260,7 @@
             projectViewFileWithSection(
                 projectViewSet,
                 TargetSection.KEY,
-                new Predicate<ListSection<TargetExpression>>() {
-                  @Override
-                  public boolean apply(@NotNull ListSection<TargetExpression> targetSection) {
-                    return targetSection.items().contains(targetExpression);
-                  }
-                });
+                targetSection -> targetSection.items().contains(targetExpression));
       }
 
       return IssueOutput.error(matcher.group(0)).inFile(file).build();
@@ -252,7 +276,7 @@
     }
 
     @Override
-    protected IssueOutput createIssue(@NotNull Matcher matcher) {
+    protected IssueOutput createIssue(Matcher matcher) {
       File file = null;
       if (projectViewSet != null) {
         final String packageString = matcher.group(1);
@@ -276,13 +300,13 @@
 
   @Nullable
   private static <T, SectionType extends Section<T>> File projectViewFileWithSection(
-      @NotNull ProjectViewSet projectViewSet,
-      @NotNull SectionKey<T, SectionType> key,
-      @NotNull Predicate<SectionType> predicate) {
+      ProjectViewSet projectViewSet,
+      SectionKey<T, SectionType> key,
+      Predicate<SectionType> predicate) {
     for (ProjectViewSet.ProjectViewFile projectViewFile : projectViewSet.getProjectViewFiles()) {
       ImmutableList<SectionType> sections = projectViewFile.projectView.getSectionsOfType(key);
       for (SectionType section : sections) {
-        if (predicate.apply(section)) {
+        if (predicate.test(section)) {
           return projectViewFile.projectViewFile;
         }
       }
@@ -290,34 +314,17 @@
     return null;
   }
 
-  @NotNull private List<Parser> parsers = Lists.newArrayList();
+  private ImmutableList<Parser> parsers;
   /**
    * The parser that requested more lines of input during the last call to {@link
    * #parseIssue(String)}.
    */
   @Nullable private Parser multilineMatchingParser;
 
-  @NotNull private List<String> multilineMatchResult = new ArrayList<>();
+  private List<String> multilineMatchResult = new ArrayList<>();
 
-  public BlazeIssueParser(@Nullable Project project, @NotNull WorkspaceRoot workspaceRoot) {
-
-    ProjectViewSet projectViewSet =
-        project != null ? ProjectViewManager.getInstance(project).getProjectViewSet() : null;
-
-    parsers.add(new CompileParser(workspaceRoot));
-    parsers.add(new TracebackParser());
-    parsers.add(new BuildParser());
-    parsers.add(new LinelessBuildParser());
-    parsers.add(new ProjectViewLabelParser(projectViewSet));
-    parsers.add(
-        new InvalidTargetProjectViewPackageParser(
-            projectViewSet, "no such package '(.*)': BUILD file not found on package path"));
-    parsers.add(
-        new InvalidTargetProjectViewPackageParser(
-            projectViewSet, "no targets found beneath '(.*)'"));
-    parsers.add(
-        new InvalidTargetProjectViewPackageParser(
-            projectViewSet, "ERROR: invalid target format '(.*)'"));
+  public BlazeIssueParser(ImmutableList<Parser> parsers) {
+    this.parsers = parsers;
   }
 
   @Nullable
diff --git a/base/src/com/google/idea/blaze/base/issueparser/IssueOutputLineProcessor.java b/base/src/com/google/idea/blaze/base/issueparser/IssueOutputLineProcessor.java
index dbb313e..c3c5de2 100644
--- a/base/src/com/google/idea/blaze/base/issueparser/IssueOutputLineProcessor.java
+++ b/base/src/com/google/idea/blaze/base/issueparser/IssueOutputLineProcessor.java
@@ -15,15 +15,17 @@
  */
 package com.google.idea.blaze.base.issueparser;
 
+import com.google.common.collect.ImmutableList;
 import com.google.idea.blaze.base.async.process.LineProcessingOutputStream;
 import com.google.idea.blaze.base.model.primitives.WorkspaceRoot;
+import com.google.idea.blaze.base.projectview.ProjectViewManager;
+import com.google.idea.blaze.base.projectview.ProjectViewSet;
 import com.google.idea.blaze.base.scope.BlazeContext;
 import com.google.idea.blaze.base.scope.output.IssueOutput;
 import com.google.idea.blaze.base.scope.output.PrintOutput;
 import com.google.idea.blaze.base.scope.output.PrintOutput.OutputType;
 import com.intellij.openapi.project.Project;
-import org.jetbrains.annotations.NotNull;
-import org.jetbrains.annotations.Nullable;
+import javax.annotation.Nullable;
 
 /**
  * Forwards output to PrintOutputs, colored by whether or not an issue is found per-line.
@@ -32,20 +34,41 @@
  */
 public class IssueOutputLineProcessor implements LineProcessingOutputStream.LineProcessor {
 
-  @NotNull private final BlazeContext context;
+  private final BlazeContext context;
 
-  @NotNull private final BlazeIssueParser blazeIssueParser;
+  private final BlazeIssueParser blazeIssueParser;
 
   public IssueOutputLineProcessor(
-      @Nullable Project project,
-      @NotNull BlazeContext context,
-      @NotNull WorkspaceRoot workspaceRoot) {
+      @Nullable Project project, BlazeContext context, WorkspaceRoot workspaceRoot) {
     this.context = context;
-    this.blazeIssueParser = new BlazeIssueParser(project, workspaceRoot);
+    ProjectViewSet projectViewSet =
+        project != null ? ProjectViewManager.getInstance(project).getProjectViewSet() : null;
+
+    ImmutableList<BlazeIssueParser.Parser> parsers =
+        ImmutableList.of(
+            new BlazeIssueParser.CompileParser(workspaceRoot),
+            new BlazeIssueParser.TracebackParser(),
+            new BlazeIssueParser.BuildParser(),
+            new BlazeIssueParser.LinelessBuildParser(),
+            new BlazeIssueParser.ProjectViewLabelParser(projectViewSet),
+            new BlazeIssueParser.InvalidTargetProjectViewPackageParser(
+                projectViewSet, "no such package '(.*)': BUILD file not found on package path"),
+            new BlazeIssueParser.InvalidTargetProjectViewPackageParser(
+                projectViewSet, "no targets found beneath '(.*)'"),
+            new BlazeIssueParser.InvalidTargetProjectViewPackageParser(
+                projectViewSet, "ERROR: invalid target format '(.*)'"),
+            new BlazeIssueParser.FileNotFoundBuildParser(workspaceRoot));
+    this.blazeIssueParser = new BlazeIssueParser(parsers);
+  }
+
+  public IssueOutputLineProcessor(
+      BlazeContext context, ImmutableList<BlazeIssueParser.Parser> parsers) {
+    this.context = context;
+    this.blazeIssueParser = new BlazeIssueParser(parsers);
   }
 
   @Override
-  public boolean processLine(@NotNull String line) {
+  public boolean processLine(String line) {
     IssueOutput issue = blazeIssueParser.parseIssue(line);
     if (issue != null) {
       if (issue.getCategory() == IssueOutput.Category.ERROR) {
diff --git a/base/src/com/google/idea/blaze/base/lang/AdditionalLanguagesHelper.java b/base/src/com/google/idea/blaze/base/lang/AdditionalLanguagesHelper.java
new file mode 100644
index 0000000..384e35b
--- /dev/null
+++ b/base/src/com/google/idea/blaze/base/lang/AdditionalLanguagesHelper.java
@@ -0,0 +1,149 @@
+/*
+ * Copyright 2016 The Bazel Authors. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.idea.blaze.base.lang;
+
+import com.google.common.collect.Sets;
+import com.google.idea.blaze.base.model.BlazeProjectData;
+import com.google.idea.blaze.base.model.primitives.LanguageClass;
+import com.google.idea.blaze.base.projectview.ProjectViewEdit;
+import com.google.idea.blaze.base.projectview.section.ListSection;
+import com.google.idea.blaze.base.projectview.section.sections.AdditionalLanguagesSection;
+import com.google.idea.blaze.base.settings.BlazeUserSettings;
+import com.google.idea.blaze.base.sync.BlazeSyncManager;
+import com.google.idea.blaze.base.sync.BlazeSyncParams;
+import com.google.idea.blaze.base.sync.data.BlazeProjectDataManager;
+import com.google.idea.blaze.base.sync.projectview.LanguageSupport;
+import com.google.idea.blaze.base.sync.projectview.WorkspaceLanguageSettings;
+import com.intellij.ide.util.PropertiesComponent;
+import com.intellij.openapi.fileEditor.FileEditor;
+import com.intellij.openapi.project.Project;
+import com.intellij.openapi.ui.Messages;
+import com.intellij.openapi.util.Key;
+import com.intellij.openapi.vfs.VirtualFile;
+import com.intellij.ui.EditorNotificationPanel;
+import com.intellij.ui.EditorNotifications;
+import java.util.Set;
+import javax.annotation.Nullable;
+
+/**
+ * Detects usage of files in supported but inactive languages, and offers to add them to the project
+ * view.
+ */
+public class AdditionalLanguagesHelper
+    extends EditorNotifications.Provider<EditorNotificationPanel> {
+
+  private static final Key<EditorNotificationPanel> KEY = Key.create("add additional language");
+
+  // avoid notifying more than once per project per language.
+  private final Set<LanguageClass> notifiedLanguages = Sets.newHashSet();
+  private final Project project;
+  private final EditorNotifications notifications;
+
+  public AdditionalLanguagesHelper(Project project, final EditorNotifications notifications) {
+    this.project = project;
+    this.notifications = notifications;
+
+    for (LanguageClass langauge : LanguageClass.values()) {
+      if (PropertiesComponent.getInstance(project).getBoolean(propertyKey(langauge))) {
+        notifiedLanguages.add(langauge);
+      }
+    }
+  }
+
+  private void suppressNotifications(LanguageClass language) {
+    PropertiesComponent.getInstance(project).setValue(propertyKey(language), true);
+    notifiedLanguages.add(language);
+    notifications.updateAllNotifications();
+  }
+
+  private static String propertyKey(LanguageClass language) {
+    return "additional_languages_helper_suppressed_" + language.getName();
+  }
+
+  @Override
+  public Key<EditorNotificationPanel> getKey() {
+    return KEY;
+  }
+
+  @Nullable
+  @Override
+  public EditorNotificationPanel createNotificationPanel(VirtualFile file, FileEditor fileEditor) {
+    String ext = file.getExtension();
+    if (ext == null) {
+      return null;
+    }
+    LanguageClass language = LanguageClass.fromExtension(ext);
+    if (language == null || notifiedLanguages.contains(language)) {
+      return null;
+    }
+    BlazeProjectData projectData =
+        BlazeProjectDataManager.getInstance(project).getBlazeProjectData();
+    if (projectData == null) {
+      return null;
+    }
+    WorkspaceLanguageSettings settings = projectData.workspaceLanguageSettings;
+    if (settings.isLanguageActive(language)) {
+      return null;
+    }
+    if (!LanguageSupport.supportedLanguagesForWorkspaceType(settings.getWorkspaceType())
+        .contains(language)) {
+      return null;
+    }
+
+    String langName = language.getName();
+    String message =
+        String.format("Do you want to enable %s support in your project view file?", langName);
+
+    EditorNotificationPanel panel = new EditorNotificationPanel();
+    panel.setText(message);
+    panel.createActionLabel(
+        String.format("Enable %s support", langName),
+        () -> enableLanguageSupport(project, language));
+    panel.createActionLabel("Don't show again", () -> suppressNotifications(language));
+    return panel;
+  }
+
+  private void enableLanguageSupport(Project project, LanguageClass language) {
+    ProjectViewEdit edit =
+        ProjectViewEdit.editLocalProjectView(
+            project,
+            builder -> {
+              ListSection<LanguageClass> existingSection =
+                  builder.getLast(AdditionalLanguagesSection.KEY);
+              builder.replace(
+                  existingSection,
+                  ListSection.update(AdditionalLanguagesSection.KEY, existingSection)
+                      .add(language));
+              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();
+
+    suppressNotifications(language);
+
+    BlazeSyncManager.getInstance(project)
+        .requestProjectSync(
+            new BlazeSyncParams.Builder("Sync", BlazeSyncParams.SyncMode.INCREMENTAL)
+                .addProjectViewTargets(true)
+                .addWorkingSet(BlazeUserSettings.getInstance().getExpandSyncToWorkingSet())
+                .build());
+  }
+}
diff --git a/base/src/com/google/idea/blaze/base/lang/buildfile/completion/BuildCompletionAutoPopupHandler.java b/base/src/com/google/idea/blaze/base/lang/buildfile/completion/BuildCompletionAutoPopupHandler.java
new file mode 100644
index 0000000..8be2b0d
--- /dev/null
+++ b/base/src/com/google/idea/blaze/base/lang/buildfile/completion/BuildCompletionAutoPopupHandler.java
@@ -0,0 +1,54 @@
+/*
+ * Copyright 2016 The Bazel Authors. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.idea.blaze.base.lang.buildfile.completion;
+
+import com.google.idea.blaze.base.lang.buildfile.psi.BuildFile;
+import com.google.idea.blaze.base.lang.buildfile.psi.StringLiteral;
+import com.intellij.codeInsight.AutoPopupController;
+import com.intellij.codeInsight.editorActions.TypedHandlerDelegate;
+import com.intellij.codeInsight.lookup.LookupManager;
+import com.intellij.openapi.editor.Editor;
+import com.intellij.openapi.project.Project;
+import com.intellij.psi.PsiElement;
+import com.intellij.psi.PsiFile;
+
+/**
+ * Supplements {@link com.intellij.codeInsight.editorActions.CompletionAutoPopupHandler}, triggering
+ * auto-complete pop-up on some non-letter characters when typing in build files.
+ */
+public class BuildCompletionAutoPopupHandler extends TypedHandlerDelegate {
+
+  @Override
+  public Result checkAutoPopup(
+      char charTyped, final Project project, final Editor editor, final PsiFile file) {
+    if (!(file instanceof BuildFile)) {
+      return Result.CONTINUE;
+    }
+    if (LookupManager.getActiveLookup(editor) != null) {
+      return Result.CONTINUE;
+    }
+
+    if (charTyped != '/' && charTyped != ':') {
+      return Result.CONTINUE;
+    }
+    PsiElement psi = file.findElementAt(editor.getCaretModel().getOffset());
+    if (psi != null && psi.getParent() instanceof StringLiteral) {
+      AutoPopupController.getInstance(project).scheduleAutoPopup(editor);
+      return Result.STOP;
+    }
+    return Result.CONTINUE;
+  }
+}
diff --git a/base/src/com/google/idea/blaze/base/lang/buildfile/completion/BuiltInFunctionCompletionContributor.java b/base/src/com/google/idea/blaze/base/lang/buildfile/completion/BuiltInFunctionCompletionContributor.java
index bf37676..4cecbca 100644
--- a/base/src/com/google/idea/blaze/base/lang/buildfile/completion/BuiltInFunctionCompletionContributor.java
+++ b/base/src/com/google/idea/blaze/base/lang/buildfile/completion/BuiltInFunctionCompletionContributor.java
@@ -18,9 +18,9 @@
 import static com.intellij.patterns.PlatformPatterns.psiComment;
 import static com.intellij.patterns.PlatformPatterns.psiElement;
 
+import com.google.common.collect.ImmutableSet;
 import com.google.idea.blaze.base.lang.buildfile.language.BuildFileLanguage;
-import com.google.idea.blaze.base.lang.buildfile.language.semantics.BuildLanguageSpec;
-import com.google.idea.blaze.base.lang.buildfile.language.semantics.BuildLanguageSpecProvider;
+import com.google.idea.blaze.base.lang.buildfile.language.semantics.BuiltInNamesProvider;
 import com.google.idea.blaze.base.lang.buildfile.psi.BuildFile;
 import com.google.idea.blaze.base.lang.buildfile.psi.FunctionStatement;
 import com.google.idea.blaze.base.lang.buildfile.psi.ReferenceExpression;
@@ -77,13 +77,9 @@
               CompletionParameters parameters,
               ProcessingContext context,
               CompletionResultSet result) {
-            BuildLanguageSpec spec =
-                BuildLanguageSpecProvider.getInstance()
-                    .getLanguageSpec(parameters.getPosition().getProject());
-            if (spec == null) {
-              return;
-            }
-            for (String ruleName : spec.getKnownRuleNames()) {
+            ImmutableSet<String> builtInNames =
+                BuiltInNamesProvider.getBuiltInFunctionNames(parameters.getPosition().getProject());
+            for (String ruleName : builtInNames) {
               result.addElement(
                   LookupElementBuilder.create(ruleName)
                       .withIcon(BlazeIcons.BuildRule)
diff --git a/base/src/com/google/idea/blaze/base/lang/buildfile/completion/BuiltInSymbolCompletionContributor.java b/base/src/com/google/idea/blaze/base/lang/buildfile/completion/BuiltInSymbolCompletionContributor.java
new file mode 100644
index 0000000..0ffe41a
--- /dev/null
+++ b/base/src/com/google/idea/blaze/base/lang/buildfile/completion/BuiltInSymbolCompletionContributor.java
@@ -0,0 +1,67 @@
+/*
+ * Copyright 2016 The Bazel Authors. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.idea.blaze.base.lang.buildfile.completion;
+
+import static com.intellij.patterns.PlatformPatterns.psiComment;
+import static com.intellij.patterns.PlatformPatterns.psiElement;
+
+import com.google.idea.blaze.base.lang.buildfile.language.BuildFileLanguage;
+import com.google.idea.blaze.base.lang.buildfile.language.semantics.BuiltInNamesProvider;
+import com.google.idea.blaze.base.lang.buildfile.psi.ReferenceExpression;
+import com.intellij.codeInsight.completion.AutoCompletionContext;
+import com.intellij.codeInsight.completion.AutoCompletionDecision;
+import com.intellij.codeInsight.completion.CompletionContributor;
+import com.intellij.codeInsight.completion.CompletionParameters;
+import com.intellij.codeInsight.completion.CompletionProvider;
+import com.intellij.codeInsight.completion.CompletionResultSet;
+import com.intellij.codeInsight.completion.CompletionType;
+import com.intellij.codeInsight.lookup.LookupElement;
+import com.intellij.codeInsight.lookup.LookupElementBuilder;
+import com.intellij.util.ProcessingContext;
+
+/** Completes built-in blaze global symbols. */
+public class BuiltInSymbolCompletionContributor extends CompletionContributor {
+
+  @Override
+  public AutoCompletionDecision handleAutoCompletionPossibility(AutoCompletionContext context) {
+    // auto-insert the obvious only case; else show other cases.
+    final LookupElement[] items = context.getItems();
+    if (items.length == 1) {
+      return AutoCompletionDecision.insertItem(items[0]);
+    }
+    return AutoCompletionDecision.SHOW_LOOKUP;
+  }
+
+  public BuiltInSymbolCompletionContributor() {
+    extend(
+        CompletionType.BASIC,
+        psiElement()
+            .withLanguage(BuildFileLanguage.INSTANCE)
+            .andNot(psiComment())
+            .withParent(ReferenceExpression.class),
+        new CompletionProvider<CompletionParameters>() {
+          @Override
+          protected void addCompletions(
+              CompletionParameters parameters,
+              ProcessingContext context,
+              CompletionResultSet result) {
+            for (String symbol : BuiltInNamesProvider.GLOBALS) {
+              result.addElement(LookupElementBuilder.create(symbol));
+            }
+          }
+        });
+  }
+}
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 8c7dcb0..c57a138 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
@@ -21,6 +21,7 @@
 import com.google.idea.blaze.base.projectview.ProjectViewSet;
 import com.google.idea.blaze.base.scope.BlazeContext;
 import com.google.idea.blaze.base.settings.BlazeImportSettings;
+import com.google.idea.blaze.base.sync.BlazeSyncParams.SyncMode;
 import com.google.idea.blaze.base.sync.SyncListener;
 import com.intellij.openapi.project.Project;
 import java.util.Map;
@@ -44,6 +45,7 @@
       BlazeImportSettings importSettings,
       ProjectViewSet projectViewSet,
       BlazeProjectData blazeProjectData,
+      SyncMode syncMode,
       SyncResult syncResult) {
     LanguageSpecResult spec = blazeProjectData.syncState.get(LanguageSpecResult.class);
     if (spec != null) {
diff --git a/base/src/com/google/idea/blaze/base/lang/buildfile/language/semantics/BuiltInNamesProvider.java b/base/src/com/google/idea/blaze/base/lang/buildfile/language/semantics/BuiltInNamesProvider.java
index 7f12f08..7b90bc2 100644
--- a/base/src/com/google/idea/blaze/base/lang/buildfile/language/semantics/BuiltInNamesProvider.java
+++ b/base/src/com/google/idea/blaze/base/lang/buildfile/language/semantics/BuiltInNamesProvider.java
@@ -25,7 +25,7 @@
 public class BuiltInNamesProvider {
 
   // https://www.bazel.io/versions/master/docs/skylark/lib/globals.html
-  private static final ImmutableSet<String> GLOBALS =
+  public static final ImmutableSet<String> GLOBALS =
       ImmutableSet.of(
           "Actions",
           "DATA_CFG",
@@ -66,11 +66,36 @@
           "type",
           "zip");
 
+  // https://www.bazel.io/versions/master/docs/be/functions.html
+  private static final ImmutableSet<String> FUNCTIONS =
+      ImmutableSet.of(
+          "load",
+          "package",
+          "pacakge_group",
+          "licenses",
+          "exports_files",
+          "glob",
+          "select",
+          "workspace");
+
+  /** Returns all built-in global symbols and function names. */
   public static ImmutableSet<String> getBuiltInNames(Project project) {
+    ImmutableSet.Builder<String> builder =
+        ImmutableSet.<String>builder().addAll(GLOBALS).addAll(FUNCTIONS);
     BuildLanguageSpec spec = BuildLanguageSpecProvider.getInstance().getLanguageSpec(project);
-    if (spec == null) {
-      return GLOBALS;
+    if (spec != null) {
+      builder = builder.addAll(spec.getKnownRuleNames());
     }
-    return ImmutableSet.<String>builder().addAll(GLOBALS).addAll(spec.getKnownRuleNames()).build();
+    return builder.build();
+  }
+
+  /** Returns all built-in rules and function names. */
+  public static ImmutableSet<String> getBuiltInFunctionNames(Project project) {
+    ImmutableSet.Builder<String> builder = ImmutableSet.<String>builder().addAll(FUNCTIONS);
+    BuildLanguageSpec spec = BuildLanguageSpecProvider.getInstance().getLanguageSpec(project);
+    if (spec != null) {
+      builder = builder.addAll(spec.getKnownRuleNames());
+    }
+    return builder.build();
   }
 }
diff --git a/base/src/com/google/idea/blaze/base/lang/buildfile/lexer/TokenKind.java b/base/src/com/google/idea/blaze/base/lang/buildfile/lexer/TokenKind.java
index 5b4517c..4887369 100644
--- a/base/src/com/google/idea/blaze/base/lang/buildfile/lexer/TokenKind.java
+++ b/base/src/com/google/idea/blaze/base/lang/buildfile/lexer/TokenKind.java
@@ -111,8 +111,8 @@
   public static final ImmutableSet<TokenKind> KEYWORDS =
       ImmutableSet.of(
           AND, AS, ASSERT, BREAK, CLASS, CONTINUE, DEF, DEL, ELIF, ELSE, EXCEPT, FINALLY, FOR, FROM,
-          GLOBAL, IF, IMPORT, IN, IS, LAMBDA, LOAD, NONLOCAL, NOT, OR, PASS, RAISE, RETURN, TRY,
-          WHILE, WITH, YIELD);
+          GLOBAL, IF, IMPORT, IN, IS, LAMBDA, NONLOCAL, NOT, OR, PASS, RAISE, RETURN, TRY, WHILE,
+          WITH, YIELD);
 
   public static final ImmutableSet<TokenKind> OPERATIONS =
       ImmutableSet.of(
diff --git a/base/src/com/google/idea/blaze/base/lang/buildfile/parser/StatementParsing.java b/base/src/com/google/idea/blaze/base/lang/buildfile/parser/StatementParsing.java
index e22e0ba..fdc8da9 100644
--- a/base/src/com/google/idea/blaze/base/lang/buildfile/parser/StatementParsing.java
+++ b/base/src/com/google/idea/blaze/base/lang/buildfile/parser/StatementParsing.java
@@ -45,7 +45,7 @@
 
   // Unlike in Python grammar, 'load' and 'def' are only allowed as a top-level statement
   public void parseTopLevelStatement() {
-    if (currentToken() == TokenKind.LOAD) {
+    if (currentToken() == TokenKind.IDENTIFIER && "load".equals(builder.getTokenText())) {
       parseLoadStatement();
     } else if (currentToken() == TokenKind.DEF) {
       parseFunctionDefStatement();
@@ -89,7 +89,7 @@
   // load '(' STRING (',' [IDENTIFIER '='] STRING)* [','] ')'
   private void parseLoadStatement() {
     PsiBuilder.Marker marker = builder.mark();
-    expect(TokenKind.LOAD);
+    expect(TokenKind.IDENTIFIER);
     expect(TokenKind.LPAREN);
     parseStringLiteral(false);
     // Not implementing [IDENTIFIER EQUALS] option -- not a documented feature,
diff --git a/base/src/com/google/idea/blaze/base/lang/buildfile/psi/FuncallExpression.java b/base/src/com/google/idea/blaze/base/lang/buildfile/psi/FuncallExpression.java
index 154d5d9..e59d9a7 100644
--- a/base/src/com/google/idea/blaze/base/lang/buildfile/psi/FuncallExpression.java
+++ b/base/src/com/google/idea/blaze/base/lang/buildfile/psi/FuncallExpression.java
@@ -129,6 +129,7 @@
     return getKeywordArgument("name");
   }
 
+  @Nullable
   public Argument.Keyword getKeywordArgument(String name) {
     ArgumentList argList = getArgList();
     return argList != null ? argList.getKeywordArgument(name) : null;
diff --git a/base/src/com/google/idea/blaze/base/model/BlazeLibrary.java b/base/src/com/google/idea/blaze/base/model/BlazeLibrary.java
index 6a227ce..d0cbe97 100644
--- a/base/src/com/google/idea/blaze/base/model/BlazeLibrary.java
+++ b/base/src/com/google/idea/blaze/base/model/BlazeLibrary.java
@@ -17,12 +17,16 @@
 
 import com.google.common.base.Objects;
 import com.google.idea.blaze.base.sync.workspace.ArtifactLocationDecoder;
+import com.intellij.openapi.application.ApplicationManager;
 import com.intellij.openapi.project.Project;
 import com.intellij.openapi.roots.libraries.Library;
 import com.intellij.openapi.util.io.FileUtil;
 import com.intellij.openapi.util.io.FileUtilRt;
+import com.intellij.openapi.vfs.LocalFileSystem;
 import com.intellij.openapi.vfs.StandardFileSystems;
 import com.intellij.openapi.vfs.VirtualFileManager;
+import com.intellij.openapi.vfs.VirtualFileSystem;
+import com.intellij.openapi.vfs.ex.temp.TempFileSystem;
 import com.intellij.util.io.URLUtil;
 import java.io.File;
 import java.io.Serializable;
@@ -73,7 +77,7 @@
         FileUtilRt.extensionEquals(name, "jar") || FileUtilRt.extensionEquals(name, "zip");
     // .jar files require an URL with "jar" protocol.
     String protocol =
-        isJarFile ? StandardFileSystems.JAR_PROTOCOL : StandardFileSystems.FILE_PROTOCOL;
+        isJarFile ? StandardFileSystems.JAR_PROTOCOL : defaultFileSystem().getProtocol();
     String filePath = FileUtil.toSystemIndependentName(path.getPath());
     String url = VirtualFileManager.constructUrl(protocol, filePath);
     if (isJarFile) {
@@ -81,4 +85,11 @@
     }
     return url;
   }
+
+  private static VirtualFileSystem defaultFileSystem() {
+    if (ApplicationManager.getApplication().isUnitTestMode()) {
+      return TempFileSystem.getInstance();
+    }
+    return LocalFileSystem.getInstance();
+  }
 }
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 afe9302..88d5199 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
@@ -48,6 +48,7 @@
   PY_BINARY("py_binary", LanguageClass.PYTHON),
   PY_TEST("py_test", LanguageClass.PYTHON),
   PY_APPENGINE_BINARY("py_appengine_binary", LanguageClass.PYTHON),
+  PY_WRAP_CC("py_wrap_cc", LanguageClass.PYTHON),
   ;
 
   static final ImmutableMap<String, Kind> STRING_TO_KIND = makeStringToKindMap();
diff --git a/base/src/com/google/idea/blaze/base/model/primitives/LanguageClass.java b/base/src/com/google/idea/blaze/base/model/primitives/LanguageClass.java
index f174a4f..9addd48 100644
--- a/base/src/com/google/idea/blaze/base/model/primitives/LanguageClass.java
+++ b/base/src/com/google/idea/blaze/base/model/primitives/LanguageClass.java
@@ -15,21 +15,40 @@
  */
 package com.google.idea.blaze.base.model.primitives;
 
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.ImmutableSet;
+import javax.annotation.Nullable;
+
 /** Language classes. */
 public enum LanguageClass {
-  GENERIC("generic"),
-  C("c"),
-  JAVA("java"),
-  ANDROID("android"),
-  JAVASCRIPT("javascript"),
-  TYPESCRIPT("typescript"),
-  DART("dart"),
-  PYTHON("python");
+  GENERIC("generic", ImmutableSet.of()),
+  C("c", ImmutableSet.of("c", "cc", "cpp", "h", "hh", "hpp")),
+  JAVA("java", ImmutableSet.of("java")),
+  ANDROID("android", ImmutableSet.of("aidl")),
+  JAVASCRIPT("javascript", ImmutableSet.of("js", "applejs")),
+  TYPESCRIPT("typescript", ImmutableSet.of("ts", "ats")),
+  DART("dart", ImmutableSet.of("dart")),
+  PYTHON("python", ImmutableSet.of("py", "pyw"));
+
+  private static final ImmutableMap<String, LanguageClass> RECOGNIZED_EXTENSIONS =
+      extensionToClassMap();
+
+  private static ImmutableMap<String, LanguageClass> extensionToClassMap() {
+    ImmutableMap.Builder<String, LanguageClass> result = ImmutableMap.builder();
+    for (LanguageClass lang : LanguageClass.values()) {
+      for (String ext : lang.recognizedFilenameExtensions) {
+        result.put(ext, lang);
+      }
+    }
+    return result.build();
+  }
 
   private final String name;
+  private final ImmutableSet<String> recognizedFilenameExtensions;
 
-  LanguageClass(String name) {
+  LanguageClass(String name, ImmutableSet<String> recognizedFilenameExtensions) {
     this.name = name;
+    this.recognizedFilenameExtensions = recognizedFilenameExtensions;
   }
 
   public String getName() {
@@ -44,4 +63,10 @@
     }
     return null;
   }
+
+  /** Returns the LanguageClass associated with the given filename extension, if it's recognized. */
+  @Nullable
+  public static LanguageClass fromExtension(String filenameExtension) {
+    return RECOGNIZED_EXTENSIONS.get(filenameExtension);
+  }
 }
diff --git a/base/src/com/google/idea/blaze/base/model/primitives/WorkspaceType.java b/base/src/com/google/idea/blaze/base/model/primitives/WorkspaceType.java
index 293aeb0..290118b 100644
--- a/base/src/com/google/idea/blaze/base/model/primitives/WorkspaceType.java
+++ b/base/src/com/google/idea/blaze/base/model/primitives/WorkspaceType.java
@@ -22,7 +22,6 @@
  * enum ordinal.
  */
 public enum WorkspaceType {
-  ANDROID_NDK("android_ndk", LanguageClass.ANDROID, LanguageClass.JAVA, LanguageClass.C),
   ANDROID("android", LanguageClass.ANDROID, LanguageClass.JAVA),
   C("c", LanguageClass.C),
   JAVA("java", LanguageClass.JAVA),
diff --git a/base/src/com/google/idea/blaze/base/projectview/section/SectionParser.java b/base/src/com/google/idea/blaze/base/projectview/section/SectionParser.java
index 6d1164b..ddfa57e 100644
--- a/base/src/com/google/idea/blaze/base/projectview/section/SectionParser.java
+++ b/base/src/com/google/idea/blaze/base/projectview/section/SectionParser.java
@@ -15,6 +15,7 @@
  */
 package com.google.idea.blaze.base.projectview.section;
 
+import com.google.idea.blaze.base.projectview.ProjectView;
 import com.google.idea.blaze.base.projectview.parser.ParseContext;
 import com.google.idea.blaze.base.projectview.parser.ProjectViewParser;
 import javax.annotation.Nullable;
@@ -46,6 +47,11 @@
     return false;
   }
 
+  /** Allows the section to add a default value. Used during the wizard. */
+  public ProjectView addProjectViewDefaultValue(ProjectView projectView) {
+    return projectView;
+  }
+
   /** The type of item(s) in this section. */
   public abstract ItemType getItemType();
 }
diff --git a/base/src/com/google/idea/blaze/base/projectview/section/sections/DirectorySection.java b/base/src/com/google/idea/blaze/base/projectview/section/sections/DirectorySection.java
index 0163e24..cc1cb23 100644
--- a/base/src/com/google/idea/blaze/base/projectview/section/sections/DirectorySection.java
+++ b/base/src/com/google/idea/blaze/base/projectview/section/sections/DirectorySection.java
@@ -17,6 +17,7 @@
 
 import com.google.common.collect.Lists;
 import com.google.idea.blaze.base.model.primitives.WorkspacePath;
+import com.google.idea.blaze.base.projectview.ProjectView;
 import com.google.idea.blaze.base.projectview.parser.ParseContext;
 import com.google.idea.blaze.base.projectview.parser.ProjectViewParser;
 import com.google.idea.blaze.base.projectview.section.ListSection;
@@ -66,5 +67,18 @@
     public ItemType getItemType() {
       return ItemType.FileSystemItem;
     }
+
+    @Override
+    public ProjectView addProjectViewDefaultValue(ProjectView projectView) {
+      if (!projectView.getSectionsOfType(KEY).isEmpty()) {
+        return projectView;
+      }
+      return ProjectView.builder(projectView)
+          .add(
+              ListSection.builder(KEY)
+                  .add(TextBlock.of("  # Add the directories you want added as source here"))
+                  .add(TextBlock.newLine()))
+          .build();
+    }
   }
 }
diff --git a/base/src/com/google/idea/blaze/base/projectview/section/sections/TargetSection.java b/base/src/com/google/idea/blaze/base/projectview/section/sections/TargetSection.java
index 5bd3361..cd69179 100644
--- a/base/src/com/google/idea/blaze/base/projectview/section/sections/TargetSection.java
+++ b/base/src/com/google/idea/blaze/base/projectview/section/sections/TargetSection.java
@@ -16,6 +16,7 @@
 package com.google.idea.blaze.base.projectview.section.sections;
 
 import com.google.idea.blaze.base.model.primitives.TargetExpression;
+import com.google.idea.blaze.base.projectview.ProjectView;
 import com.google.idea.blaze.base.projectview.parser.ParseContext;
 import com.google.idea.blaze.base.projectview.parser.ProjectViewParser;
 import com.google.idea.blaze.base.projectview.section.ListSection;
@@ -49,5 +50,21 @@
     public ItemType getItemType() {
       return ItemType.Label;
     }
+
+    @Override
+    public ProjectView addProjectViewDefaultValue(ProjectView projectView) {
+      if (!projectView.getSectionsOfType(KEY).isEmpty()) {
+        return projectView;
+      }
+      return ProjectView.builder(projectView)
+          .add(
+              ListSection.builder(KEY)
+                  .add(
+                      TextBlock.of(
+                          "  # Add targets that reach the source code "
+                              + "that you want to resolve here"))
+                  .add(TextBlock.newLine()))
+          .build();
+    }
   }
 }
diff --git a/base/src/com/google/idea/blaze/base/run/BlazeRunConfigurationSyncListener.java b/base/src/com/google/idea/blaze/base/run/BlazeRunConfigurationSyncListener.java
old mode 100755
new mode 100644
index 80ae557..74a3812
--- a/base/src/com/google/idea/blaze/base/run/BlazeRunConfigurationSyncListener.java
+++ b/base/src/com/google/idea/blaze/base/run/BlazeRunConfigurationSyncListener.java
@@ -23,6 +23,7 @@
 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.BlazeSyncParams.SyncMode;
 import com.google.idea.blaze.base.sync.SyncListener;
 import com.intellij.execution.RunManager;
 import com.intellij.execution.RunnerAndConfigurationSettings;
@@ -42,6 +43,7 @@
       BlazeImportSettings importSettings,
       ProjectViewSet projectViewSet,
       BlazeProjectData blazeProjectData,
+      SyncMode syncMode,
       SyncResult syncResult) {
 
     UIUtil.invokeAndWaitIfNeeded(
diff --git a/base/src/com/google/idea/blaze/base/run/testmap/TestTargetFilterImpl.java b/base/src/com/google/idea/blaze/base/run/testmap/TestTargetFilterImpl.java
index c0d9f1a..0b336f4 100644
--- a/base/src/com/google/idea/blaze/base/run/testmap/TestTargetFilterImpl.java
+++ b/base/src/com/google/idea/blaze/base/run/testmap/TestTargetFilterImpl.java
@@ -34,6 +34,7 @@
 import com.google.idea.blaze.base.run.TestTargetFinder;
 import com.google.idea.blaze.base.scope.BlazeContext;
 import com.google.idea.blaze.base.settings.BlazeImportSettings;
+import com.google.idea.blaze.base.sync.BlazeSyncParams.SyncMode;
 import com.google.idea.blaze.base.sync.SyncListener;
 import com.google.idea.blaze.base.sync.data.BlazeProjectDataManager;
 import com.google.idea.blaze.base.sync.workspace.ArtifactLocationDecoder;
@@ -180,6 +181,7 @@
         BlazeImportSettings importSettings,
         ProjectViewSet projectViewSet,
         BlazeProjectData blazeProjectData,
+        SyncMode syncMode,
         SyncResult syncResult) {
       TestTargetFinder testTargetFinder = TestTargetFinder.getInstance(project);
       ((TestTargetFilterImpl) testTargetFinder).clearMapData();
diff --git a/base/src/com/google/idea/blaze/base/scope/BlazeContext.java b/base/src/com/google/idea/blaze/base/scope/BlazeContext.java
index 82568ae..316c05b 100644
--- a/base/src/com/google/idea/blaze/base/scope/BlazeContext.java
+++ b/base/src/com/google/idea/blaze/base/scope/BlazeContext.java
@@ -17,6 +17,7 @@
 
 import com.google.common.annotations.VisibleForTesting;
 import com.google.common.collect.ArrayListMultimap;
+import com.google.common.collect.ListMultimap;
 import com.google.common.collect.Lists;
 import java.util.List;
 import org.jetbrains.annotations.NotNull;
@@ -29,7 +30,7 @@
   @NotNull private final List<BlazeScope> scopes = Lists.newArrayList();
 
   @NotNull
-  private final ArrayListMultimap<Class<? extends Output>, OutputSink<?>> outputSinks =
+  private final ListMultimap<Class<? extends Output>, OutputSink<?>> outputSinks =
       ArrayListMultimap.create();
 
   boolean isEnding;
diff --git a/base/src/com/google/idea/blaze/base/settings/BlazeUserSettings.java b/base/src/com/google/idea/blaze/base/settings/BlazeUserSettings.java
index 1e74ef4..6b7298e 100644
--- a/base/src/com/google/idea/blaze/base/settings/BlazeUserSettings.java
+++ b/base/src/com/google/idea/blaze/base/settings/BlazeUserSettings.java
@@ -36,7 +36,7 @@
 )
 public class BlazeUserSettings implements PersistentStateComponent<BlazeUserSettings> {
 
-  public boolean suppressConsoleForRunAction = false;
+  private boolean suppressConsoleForRunAction = false;
   private boolean resyncAutomatically = false;
   private boolean syncStatusPopupShown = false;
   private boolean expandSyncToWorkingSet = true;
@@ -44,6 +44,7 @@
   private boolean attachSourcesByDefault = false;
   private boolean attachSourcesOnDemand = false;
   private boolean collapseProjectView = true;
+  private boolean formatBuildFilesOnSave = true;
   private String blazeBinaryPath = "/usr/bin/blaze";
   @Nullable private String bazelBinaryPath;
 
@@ -141,6 +142,14 @@
     this.collapseProjectView = collapseProjectView;
   }
 
+  public boolean getFormatBuildFilesOnSave() {
+    return formatBuildFilesOnSave;
+  }
+
+  public void setFormatBuildFilesOnSave(boolean formatBuildFilesOnSave) {
+    this.formatBuildFilesOnSave = formatBuildFilesOnSave;
+  }
+
   // Deprecated -- use BlazeJavaUserSettings
   @Deprecated
   @SuppressWarnings("unused") // Used by bean serialization
diff --git a/base/src/com/google/idea/blaze/base/settings/ui/BlazeUserSettingsConfigurable.java b/base/src/com/google/idea/blaze/base/settings/ui/BlazeUserSettingsConfigurable.java
index 82e45fa..f8a7210 100644
--- a/base/src/com/google/idea/blaze/base/settings/ui/BlazeUserSettingsConfigurable.java
+++ b/base/src/com/google/idea/blaze/base/settings/ui/BlazeUserSettingsConfigurable.java
@@ -52,6 +52,7 @@
   private JCheckBox suppressConsoleForRunAction;
   private JCheckBox resyncAutomatically;
   private JCheckBox collapseProjectView;
+  private JCheckBox formatBuildFilesOnSave;
   private FileSelectorWithStoredHistory blazeBinaryPathField;
   private FileSelectorWithStoredHistory bazelBinaryPathField;
 
@@ -83,6 +84,7 @@
     settings.setSuppressConsoleForRunAction(suppressConsoleForRunAction.isSelected());
     settings.setResyncAutomatically(resyncAutomatically.isSelected());
     settings.setCollapseProjectView(collapseProjectView.isSelected());
+    settings.setFormatBuildFilesOnSave(formatBuildFilesOnSave.isSelected());
     settings.setBlazeBinaryPath(Strings.nullToEmpty(blazeBinaryPathField.getText()));
     settings.setBazelBinaryPath(Strings.nullToEmpty(bazelBinaryPathField.getText()));
 
@@ -97,6 +99,7 @@
     suppressConsoleForRunAction.setSelected(settings.getSuppressConsoleForRunAction());
     resyncAutomatically.setSelected(settings.getResyncAutomatically());
     collapseProjectView.setSelected(settings.getCollapseProjectView());
+    formatBuildFilesOnSave.setSelected(settings.getFormatBuildFilesOnSave());
     blazeBinaryPathField.setTextWithHistory(settings.getBlazeBinaryPath());
     bazelBinaryPathField.setTextWithHistory(settings.getBazelBinaryPath());
 
@@ -115,10 +118,10 @@
   public boolean isModified() {
     BlazeUserSettings settings = BlazeUserSettings.getInstance();
     boolean isModified =
-        !Objects.equal(
-                suppressConsoleForRunAction.isSelected(), settings.getSuppressConsoleForRunAction())
-            || !Objects.equal(resyncAutomatically.isSelected(), settings.getResyncAutomatically())
-            || !Objects.equal(collapseProjectView.isSelected(), settings.getCollapseProjectView())
+        suppressConsoleForRunAction.isSelected() != settings.getSuppressConsoleForRunAction()
+            || resyncAutomatically.isSelected() != settings.getResyncAutomatically()
+            || collapseProjectView.isSelected() != settings.getCollapseProjectView()
+            || formatBuildFilesOnSave.isSelected() != settings.getFormatBuildFilesOnSave()
             || !Objects.equal(
                 Strings.nullToEmpty(blazeBinaryPathField.getText()),
                 Strings.nullToEmpty(settings.getBlazeBinaryPath()))
@@ -153,7 +156,7 @@
       contributorRowCount += contributor.getRowCount();
     }
 
-    final int totalRowSize = 6 + contributorRowCount;
+    final int totalRowSize = 7 + contributorRowCount;
     int rowi = 0;
 
     myMainPanel = new JPanel();
@@ -216,6 +219,25 @@
             null,
             0,
             false));
+    formatBuildFilesOnSave = new JCheckBox();
+    formatBuildFilesOnSave.setSelected(false);
+    formatBuildFilesOnSave.setText("Automatically format BUILD files on file save");
+    myMainPanel.add(
+        formatBuildFilesOnSave,
+        new GridConstraints(
+            rowi++,
+            0,
+            1,
+            2,
+            GridConstraints.ANCHOR_NORTHWEST,
+            GridConstraints.FILL_NONE,
+            GridConstraints.SIZEPOLICY_CAN_SHRINK | GridConstraints.SIZEPOLICY_CAN_GROW,
+            GridConstraints.SIZEPOLICY_FIXED,
+            null,
+            null,
+            null,
+            0,
+            false));
     for (BlazeUserSettingsContributor contributor : settingsContributors) {
       rowi = contributor.addComponents(myMainPanel, rowi);
     }
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 b29ce17..0b55343 100644
--- a/base/src/com/google/idea/blaze/base/sync/BlazeSyncPlugin.java
+++ b/base/src/com/google/idea/blaze/base/sync/BlazeSyncPlugin.java
@@ -36,7 +36,6 @@
 import com.intellij.openapi.module.Module;
 import com.intellij.openapi.module.ModuleType;
 import com.intellij.openapi.project.Project;
-import com.intellij.openapi.roots.ContentEntry;
 import com.intellij.openapi.roots.ModifiableRootModel;
 import java.util.Collection;
 import java.util.Set;
@@ -122,17 +121,8 @@
       ProjectViewSet projectViewSet,
       BlazeProjectData blazeProjectData);
 
-  /**
-   * Modify the project content entries. There will be one content entry per project directory from
-   * the project view set.
-   */
-  void updateContentEntries(
-      Project project,
-      BlazeContext context,
-      WorkspaceRoot workspaceRoot,
-      ProjectViewSet projectViewSet,
-      BlazeProjectData blazeProjectData,
-      Collection<ContentEntry> contentEntries);
+  @Nullable
+  SourceFolderProvider getSourceFolderProvider(BlazeProjectData projectData);
 
   /** Modifies the IDE project structure in accordance with the sync data. */
   void updateProjectStructure(
@@ -215,14 +205,11 @@
         ProjectViewSet projectViewSet,
         BlazeProjectData blazeProjectData) {}
 
+    @Nullable
     @Override
-    public void updateContentEntries(
-        Project project,
-        BlazeContext context,
-        WorkspaceRoot workspaceRoot,
-        ProjectViewSet projectViewSet,
-        BlazeProjectData blazeProjectData,
-        Collection<ContentEntry> contentEntries) {}
+    public SourceFolderProvider getSourceFolderProvider(BlazeProjectData projectData) {
+      return null;
+    }
 
     @Override
     public void updateProjectStructure(
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 68ee51d..3d4e3ec 100644
--- a/base/src/com/google/idea/blaze/base/sync/BlazeSyncTask.java
+++ b/base/src/com/google/idea/blaze/base/sync/BlazeSyncTask.java
@@ -84,6 +84,7 @@
 import com.google.idea.blaze.base.targetmaps.ReverseDependencyMap;
 import com.google.idea.blaze.base.util.SaveUtil;
 import com.google.idea.blaze.base.vcs.BlazeVcsHandler;
+import com.intellij.openapi.application.ApplicationManager;
 import com.intellij.openapi.diagnostic.Logger;
 import com.intellij.openapi.module.Module;
 import com.intellij.openapi.module.ModuleType;
@@ -94,8 +95,10 @@
 import com.intellij.openapi.roots.ModifiableRootModel;
 import com.intellij.openapi.roots.ex.ProjectRootManagerEx;
 import com.intellij.openapi.util.io.FileUtil;
-import com.intellij.openapi.vfs.StandardFileSystems;
+import com.intellij.openapi.vfs.LocalFileSystem;
 import com.intellij.openapi.vfs.VirtualFileManager;
+import com.intellij.openapi.vfs.VirtualFileSystem;
+import com.intellij.openapi.vfs.ex.temp.TempFileSystem;
 import java.io.File;
 import java.util.Collection;
 import java.util.Collections;
@@ -157,37 +160,34 @@
   @VisibleForTesting
   boolean syncProject(BlazeContext context) {
     SyncResult syncResult = SyncResult.FAILURE;
+    SyncMode syncMode = syncParams.syncMode;
     try {
       SaveUtil.saveAllFiles();
-      onSyncStart(project, context);
-      syncResult = doSyncProject(context);
+      BlazeProjectData oldBlazeProjectData =
+          syncMode != SyncMode.FULL
+              ? BlazeProjectDataManagerImpl.getImpl(project)
+                  .loadProjectRoot(context, importSettings)
+              : null;
+      if (oldBlazeProjectData == null) {
+        syncMode = SyncMode.FULL;
+      }
+
+      onSyncStart(project, context, syncMode);
+      syncResult = doSyncProject(context, syncMode, oldBlazeProjectData);
     } catch (AssertionError | Exception e) {
       LOG.error(e);
       IssueOutput.error("Internal error: " + e.getMessage()).submit(context);
     } finally {
-      afterSync(project, context, syncResult);
+      afterSync(project, context, syncMode, syncResult);
     }
     return syncResult == SyncResult.SUCCESS || syncResult == SyncResult.PARTIAL_SUCCESS;
   }
 
   /** @return true if sync successfully completed */
-  private SyncResult doSyncProject(final BlazeContext context) {
+  private SyncResult doSyncProject(
+      BlazeContext context, SyncMode syncMode, @Nullable BlazeProjectData oldBlazeProjectData) {
     this.syncStartTime = System.currentTimeMillis();
 
-    if (importSettings.getProjectViewFile() == null) {
-      IssueOutput.error(
-              "This project looks like it's been opened from an old version of ASwB. "
-                  + "That is unfortunately not supported. Please reimport your project.")
-          .submit(context);
-      return SyncResult.FAILURE;
-    }
-
-    @Nullable BlazeProjectData oldBlazeProjectData = null;
-    if (syncParams.syncMode != SyncMode.FULL) {
-      oldBlazeProjectData =
-          BlazeProjectDataManagerImpl.getImpl(project).loadProjectRoot(context, importSettings);
-    }
-
     BlazeVcsHandler vcsHandler = null;
     for (BlazeVcsHandler candidate : BlazeVcsHandler.EP_NAME.getExtensions()) {
       if (candidate.handlesProject(importSettings.getBuildSystem(), workspaceRoot)) {
@@ -271,7 +271,7 @@
 
     BuildResult ideInfoResult = BuildResult.SUCCESS;
     BuildResult ideResolveResult = BuildResult.SUCCESS;
-    if (syncParams.syncMode != SyncMode.RESTORE_EPHEMERAL_STATE || oldBlazeProjectData == null) {
+    if (syncMode != SyncMode.RESTORE_EPHEMERAL_STATE || oldBlazeProjectData == null) {
       SyncState.Builder syncStateBuilder = new SyncState.Builder();
       SyncState previousSyncState =
           oldBlazeProjectData != null ? oldBlazeProjectData.syncState : null;
@@ -384,7 +384,7 @@
       newBlazeProjectData = oldBlazeProjectData;
     }
 
-    FileCaches.onSync(project, context, projectViewSet, newBlazeProjectData, syncParams.syncMode);
+    FileCaches.onSync(project, context, projectViewSet, newBlazeProjectData, syncMode);
     ListenableFuture<?> prefetch =
         PrefetchService.getInstance().prefetchProjectFiles(project, newBlazeProjectData);
     FutureUtil.waitForFuture(context, prefetch)
@@ -417,7 +417,7 @@
       syncResult = SyncResult.PARTIAL_SUCCESS;
     }
 
-    onSyncComplete(project, context, projectViewSet, newBlazeProjectData, syncResult);
+    onSyncComplete(project, context, projectViewSet, newBlazeProjectData, syncMode, syncResult);
     return syncResult;
   }
 
@@ -681,7 +681,8 @@
       IssueOutput.warn("Could not set module type for workspace module.").submit(context);
     }
 
-    Module workspaceModule = moduleEditor.createModule(".workspace", workspaceModuleType);
+    Module workspaceModule =
+        moduleEditor.createModule(BlazeDataStorage.WORKSPACE_MODULE_NAME, workspaceModuleType);
     ModifiableRootModel workspaceModifiableModel = moduleEditor.editModule(workspaceModule);
 
     ContentEntryEditor.createContentEntries(
@@ -734,20 +735,28 @@
 
   private static String pathToUrl(File path) {
     String filePath = FileUtil.toSystemIndependentName(path.getPath());
-    return VirtualFileManager.constructUrl(StandardFileSystems.FILE_PROTOCOL, filePath);
+    return VirtualFileManager.constructUrl(defaultFileSystem().getProtocol(), filePath);
   }
 
-  private static void onSyncStart(Project project, BlazeContext context) {
+  private static VirtualFileSystem defaultFileSystem() {
+    if (ApplicationManager.getApplication().isUnitTestMode()) {
+      return TempFileSystem.getInstance();
+    }
+    return LocalFileSystem.getInstance();
+  }
+
+  private static void onSyncStart(Project project, BlazeContext context, SyncMode syncMode) {
     final SyncListener[] syncListeners = SyncListener.EP_NAME.getExtensions();
     for (SyncListener syncListener : syncListeners) {
-      syncListener.onSyncStart(project, context);
+      syncListener.onSyncStart(project, context, syncMode);
     }
   }
 
-  private static void afterSync(Project project, BlazeContext context, SyncResult syncResult) {
+  private static void afterSync(
+      Project project, BlazeContext context, SyncMode syncMode, SyncResult syncResult) {
     final SyncListener[] syncListeners = SyncListener.EP_NAME.getExtensions();
     for (SyncListener syncListener : syncListeners) {
-      syncListener.afterSync(project, context, syncResult);
+      syncListener.afterSync(project, context, syncMode, syncResult);
     }
   }
 
@@ -756,13 +765,14 @@
       BlazeContext context,
       ProjectViewSet projectViewSet,
       BlazeProjectData blazeProjectData,
+      SyncMode syncMode,
       SyncResult syncResult) {
     validate(project, context, blazeProjectData);
 
     final SyncListener[] syncListeners = SyncListener.EP_NAME.getExtensions();
     for (SyncListener syncListener : syncListeners) {
       syncListener.onSyncComplete(
-          project, context, importSettings, projectViewSet, blazeProjectData, syncResult);
+          project, context, importSettings, projectViewSet, blazeProjectData, syncMode, syncResult);
     }
   }
 
diff --git a/base/src/com/google/idea/blaze/base/sync/GenericSourceFolderProvider.java b/base/src/com/google/idea/blaze/base/sync/GenericSourceFolderProvider.java
new file mode 100644
index 0000000..1810e9c
--- /dev/null
+++ b/base/src/com/google/idea/blaze/base/sync/GenericSourceFolderProvider.java
@@ -0,0 +1,49 @@
+/*
+ * Copyright 2016 The Bazel Authors. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.idea.blaze.base.sync;
+
+import com.google.common.collect.ImmutableMap;
+import com.intellij.openapi.roots.ContentEntry;
+import com.intellij.openapi.roots.SourceFolder;
+import com.intellij.openapi.vfs.VirtualFile;
+
+/** An implementation of {@link SourceFolderProvider} with no language-specific settings. */
+public class GenericSourceFolderProvider implements SourceFolderProvider {
+
+  public static final GenericSourceFolderProvider INSTANCE = new GenericSourceFolderProvider();
+
+  private GenericSourceFolderProvider() {}
+
+  @Override
+  public ImmutableMap<VirtualFile, SourceFolder> initializeSourceFolders(
+      ContentEntry contentEntry) {
+    ImmutableMap.Builder<VirtualFile, SourceFolder> output = ImmutableMap.builder();
+    VirtualFile file = contentEntry.getFile();
+    if (file != null) {
+      output.put(file, contentEntry.addSourceFolder(file, false));
+    }
+    return output.build();
+  }
+
+  @Override
+  public SourceFolder setSourceFolderForLocation(
+      ContentEntry contentEntry,
+      SourceFolder parentFolder,
+      VirtualFile file,
+      boolean isTestSource) {
+    return contentEntry.addSourceFolder(file, isTestSource);
+  }
+}
diff --git a/base/src/com/google/idea/blaze/base/sync/SourceFolderProvider.java b/base/src/com/google/idea/blaze/base/sync/SourceFolderProvider.java
new file mode 100644
index 0000000..6514a82
--- /dev/null
+++ b/base/src/com/google/idea/blaze/base/sync/SourceFolderProvider.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.base.sync;
+
+import com.google.common.collect.ImmutableMap;
+import com.google.idea.blaze.base.model.BlazeProjectData;
+import com.intellij.openapi.roots.ContentEntry;
+import com.intellij.openapi.roots.SourceFolder;
+import com.intellij.openapi.vfs.VirtualFile;
+
+/** Provides source folders for each content entry during sync. */
+public interface SourceFolderProvider {
+
+  /** Iterates over the available sync plugins, requesting a SourceFolderProvider. */
+  static SourceFolderProvider getSourceFolderProvider(BlazeProjectData projectData) {
+    for (BlazeSyncPlugin syncPlugin : BlazeSyncPlugin.EP_NAME.getExtensions()) {
+      SourceFolderProvider provider = syncPlugin.getSourceFolderProvider(projectData);
+      if (provider != null) {
+        return provider;
+      }
+    }
+    throw new RuntimeException(
+        "No SourceFolderProvider available for workspace type: "
+            + projectData.workspaceLanguageSettings.getWorkspaceType());
+  }
+
+  /**
+   * Creates the initial source folders for the given {@link ContentEntry}. These source folders are
+   * 'initial' because the 'is test' property (and potentially additional test source folders) are
+   * added later.
+   */
+  ImmutableMap<VirtualFile, SourceFolder> initializeSourceFolders(ContentEntry contentEntry);
+
+  /**
+   * Sets the source folder for the given file, incorporating the test information as appropriate.
+   */
+  SourceFolder setSourceFolderForLocation(
+      ContentEntry contentEntry, SourceFolder parentFolder, VirtualFile file, boolean isTestSource);
+}
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 28ba5fc..a3512cb 100644
--- a/base/src/com/google/idea/blaze/base/sync/SyncListener.java
+++ b/base/src/com/google/idea/blaze/base/sync/SyncListener.java
@@ -19,6 +19,7 @@
 import com.google.idea.blaze.base.projectview.ProjectViewSet;
 import com.google.idea.blaze.base.scope.BlazeContext;
 import com.google.idea.blaze.base.settings.BlazeImportSettings;
+import com.google.idea.blaze.base.sync.BlazeSyncParams.SyncMode;
 import com.intellij.openapi.extensions.ExtensionPointName;
 import com.intellij.openapi.project.Project;
 
@@ -40,7 +41,7 @@
   }
 
   /** Called after open documents have been saved, prior to starting the blaze sync. */
-  void onSyncStart(Project project, BlazeContext context);
+  void onSyncStart(Project project, BlazeContext context, SyncMode syncMode);
 
   /** Called on successful (or partially successful) completion of a sync */
   void onSyncComplete(
@@ -49,16 +50,17 @@
       BlazeImportSettings importSettings,
       ProjectViewSet projectViewSet,
       BlazeProjectData blazeProjectData,
+      SyncMode syncMode,
       SyncResult syncResult);
 
   /** Guaranteed to be called once per sync, regardless of whether it successfully completed */
-  void afterSync(Project project, BlazeContext context, SyncResult syncResult);
+  void afterSync(Project project, BlazeContext context, SyncMode syncMode, SyncResult syncResult);
 
   /** Convenience adapter class. */
   abstract class Adapter implements SyncListener {
 
     @Override
-    public void onSyncStart(Project project, BlazeContext context) {}
+    public void onSyncStart(Project project, BlazeContext context, SyncMode syncMode) {}
 
     @Override
     public void onSyncComplete(
@@ -67,9 +69,11 @@
         BlazeImportSettings importSettings,
         ProjectViewSet projectViewSet,
         BlazeProjectData blazeProjectData,
+        SyncMode syncMode,
         SyncResult syncResult) {}
 
     @Override
-    public void afterSync(Project project, BlazeContext context, SyncResult syncResult) {}
+    public void afterSync(
+        Project project, BlazeContext context, SyncMode syncMode, SyncResult syncResult) {}
   }
 }
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 fdc584d..f44f200 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
@@ -402,7 +402,7 @@
     return BuildResult.fromExitCode(retVal);
   }
 
-  private AspectStrategy getAspectStrategy(Project project) {
+  private static AspectStrategy getAspectStrategy(Project project) {
     for (AspectStrategyProvider provider : AspectStrategyProvider.EP_NAME.getExtensions()) {
       AspectStrategy aspectStrategy = provider.getAspectStrategy(project);
       if (aspectStrategy != null) {
diff --git a/base/src/com/google/idea/blaze/base/sync/aspects/strategy/AspectStrategyProvider.java b/base/src/com/google/idea/blaze/base/sync/aspects/strategy/AspectStrategyProvider.java
index 960a79c..0fdd2b5 100644
--- a/base/src/com/google/idea/blaze/base/sync/aspects/strategy/AspectStrategyProvider.java
+++ b/base/src/com/google/idea/blaze/base/sync/aspects/strategy/AspectStrategyProvider.java
@@ -17,11 +17,13 @@
 
 import com.intellij.openapi.extensions.ExtensionPointName;
 import com.intellij.openapi.project.Project;
+import javax.annotation.Nullable;
 
 /** Extension point for providing an aspect strategy */
 public interface AspectStrategyProvider {
   ExtensionPointName<AspectStrategyProvider> EP_NAME =
       ExtensionPointName.create("com.google.idea.blaze.AspectStrategyProvider");
 
+  @Nullable
   AspectStrategy getAspectStrategy(Project project);
 }
diff --git a/base/src/com/google/idea/blaze/base/sync/data/BlazeDataStorage.java b/base/src/com/google/idea/blaze/base/sync/data/BlazeDataStorage.java
index 2a9fca8..b86e4f7 100644
--- a/base/src/com/google/idea/blaze/base/sync/data/BlazeDataStorage.java
+++ b/base/src/com/google/idea/blaze/base/sync/data/BlazeDataStorage.java
@@ -24,6 +24,7 @@
 /** Defines where we store our blaze project data. */
 public class BlazeDataStorage {
   private static final String DATA_SUBDIRECTORY = ".blaze";
+  public static final String WORKSPACE_MODULE_NAME = ".workspace";
 
   public static File getProjectDataDir(BlazeImportSettings importSettings) {
     return new File(importSettings.getProjectDataDirectory(), DATA_SUBDIRECTORY);
diff --git a/base/src/com/google/idea/blaze/base/sync/projectstructure/ContentEntryEditor.java b/base/src/com/google/idea/blaze/base/sync/projectstructure/ContentEntryEditor.java
index e3e4357..871274e 100644
--- a/base/src/com/google/idea/blaze/base/sync/projectstructure/ContentEntryEditor.java
+++ b/base/src/com/google/idea/blaze/base/sync/projectstructure/ContentEntryEditor.java
@@ -16,6 +16,7 @@
 package com.google.idea.blaze.base.sync.projectstructure;
 
 import com.google.common.collect.ArrayListMultimap;
+import com.google.common.collect.ImmutableMap;
 import com.google.common.collect.Lists;
 import com.google.common.collect.Multimap;
 import com.google.idea.blaze.base.model.BlazeProjectData;
@@ -24,20 +25,26 @@
 import com.google.idea.blaze.base.projectview.ProjectViewSet;
 import com.google.idea.blaze.base.scope.BlazeContext;
 import com.google.idea.blaze.base.settings.Blaze;
-import com.google.idea.blaze.base.sync.BlazeSyncPlugin;
+import com.google.idea.blaze.base.sync.SourceFolderProvider;
 import com.google.idea.blaze.base.sync.projectview.ImportRoots;
+import com.google.idea.blaze.base.sync.projectview.SourceTestConfig;
+import com.intellij.openapi.application.ApplicationManager;
 import com.intellij.openapi.project.Project;
 import com.intellij.openapi.roots.ContentEntry;
 import com.intellij.openapi.roots.ModifiableRootModel;
+import com.intellij.openapi.roots.SourceFolder;
 import com.intellij.openapi.util.io.FileUtil;
 import com.intellij.openapi.util.io.FileUtilRt;
-import com.intellij.openapi.vfs.VfsUtilCore;
+import com.intellij.openapi.vfs.LocalFileSystem;
+import com.intellij.openapi.vfs.VirtualFile;
+import com.intellij.openapi.vfs.VirtualFileManager;
+import com.intellij.openapi.vfs.VirtualFileSystem;
+import com.intellij.openapi.vfs.ex.temp.TempFileSystem;
 import com.intellij.util.io.URLUtil;
 import java.io.File;
 import java.util.Collection;
 import java.util.List;
-import org.jetbrains.annotations.NonNls;
-import org.jetbrains.annotations.NotNull;
+import javax.annotation.Nullable;
 
 /** Modifies content entries based on project data. */
 public class ContentEntryEditor {
@@ -58,6 +65,9 @@
     Multimap<WorkspacePath, WorkspacePath> excludesByRootDirectory =
         sortExcludesByRootDirectory(rootDirectories, excludeDirectories);
 
+    SourceTestConfig testConfig = new SourceTestConfig(projectViewSet);
+    SourceFolderProvider provider = SourceFolderProvider.getSourceFolderProvider(blazeProjectData);
+
     List<ContentEntry> contentEntries = Lists.newArrayList();
     for (WorkspacePath rootDirectory : rootDirectories) {
       File root = workspaceRoot.fileForPath(rootDirectory);
@@ -68,14 +78,80 @@
         File excludeFolder = workspaceRoot.fileForPath(exclude);
         contentEntry.addExcludeFolder(pathToIdeaUrl(excludeFolder));
       }
-    }
 
-    for (BlazeSyncPlugin syncPlugin : BlazeSyncPlugin.EP_NAME.getExtensions()) {
-      syncPlugin.updateContentEntries(
-          project, context, workspaceRoot, projectViewSet, blazeProjectData, contentEntries);
+      ImmutableMap<VirtualFile, SourceFolder> sourceFolders =
+          provider.initializeSourceFolders(contentEntry);
+      VirtualFile rootFile = getVirtualFile(root);
+      SourceFolder rootSource = sourceFolders.get(rootFile);
+      walkFileSystem(
+          workspaceRoot,
+          testConfig,
+          excludesByRootDirectory.get(rootDirectory),
+          contentEntry,
+          provider,
+          sourceFolders,
+          rootSource,
+          rootFile);
     }
   }
 
+  private static void walkFileSystem(
+      WorkspaceRoot workspaceRoot,
+      SourceTestConfig testConfig,
+      Collection<WorkspacePath> excludedDirectories,
+      ContentEntry contentEntry,
+      SourceFolderProvider provider,
+      ImmutableMap<VirtualFile, SourceFolder> sourceFolders,
+      SourceFolder parent,
+      VirtualFile file) {
+    if (!file.isDirectory()) {
+      return;
+    }
+    WorkspacePath workspacePath;
+    try {
+      workspacePath = workspaceRoot.workspacePathFor(file);
+    } catch (IllegalArgumentException e) {
+      // stop at directories with unhandled characters.
+      return;
+    }
+    if (excludedDirectories.contains(workspacePath)) {
+      return;
+    }
+    boolean isTest = testConfig.isTestSource(workspacePath.relativePath());
+    SourceFolder current = sourceFolders.get(file);
+    SourceFolder currentOrParent = current != null ? current : parent;
+    if (isTest != currentOrParent.isTestSource()) {
+      if (current != null) {
+        contentEntry.removeSourceFolder(current);
+      }
+      currentOrParent =
+          provider.setSourceFolderForLocation(contentEntry, currentOrParent, file, isTest);
+    }
+    for (VirtualFile child : file.getChildren()) {
+      walkFileSystem(
+          workspaceRoot,
+          testConfig,
+          excludedDirectories,
+          contentEntry,
+          provider,
+          sourceFolders,
+          currentOrParent,
+          child);
+    }
+  }
+
+  @Nullable
+  private static VirtualFile getVirtualFile(File file) {
+    return getFileSystem().findFileByPath(file.getPath());
+  }
+
+  private static VirtualFileSystem getFileSystem() {
+    if (ApplicationManager.getApplication().isUnitTestMode()) {
+      return TempFileSystem.getInstance();
+    }
+    return LocalFileSystem.getInstance();
+  }
+
   private static Multimap<WorkspacePath, WorkspacePath> sortExcludesByRootDirectory(
       Collection<WorkspacePath> rootDirectories, Collection<WorkspacePath> excludedDirectories) {
 
@@ -104,25 +180,29 @@
             || (relativePath.charAt(rootDirectoryString.length()) == '/'));
   }
 
-  @NotNull
-  private static String pathToUrl(@NotNull String filePath) {
+  private static String pathToUrl(String filePath) {
     filePath = FileUtil.toSystemIndependentName(filePath);
     if (filePath.endsWith(".srcjar") || filePath.endsWith(".jar")) {
       return URLUtil.JAR_PROTOCOL + URLUtil.SCHEME_SEPARATOR + filePath + URLUtil.JAR_SEPARATOR;
     } else if (filePath.contains("src.jar!")) {
       return URLUtil.JAR_PROTOCOL + URLUtil.SCHEME_SEPARATOR + filePath;
     } else {
-      return VfsUtilCore.pathToUrl(filePath);
+      return VirtualFileManager.constructUrl(defaultFileSystem().getProtocol(), filePath);
     }
   }
 
-  @NotNull
-  private static String pathToIdeaUrl(@NotNull File path) {
+  private static VirtualFileSystem defaultFileSystem() {
+    if (ApplicationManager.getApplication().isUnitTestMode()) {
+      return TempFileSystem.getInstance();
+    }
+    return LocalFileSystem.getInstance();
+  }
+
+  private static String pathToIdeaUrl(File path) {
     return pathToUrl(toSystemIndependentName(path.getPath()));
   }
 
-  @NotNull
-  private static String toSystemIndependentName(@NonNls @NotNull String aFileName) {
+  private static String toSystemIndependentName(String aFileName) {
     return FileUtilRt.toSystemIndependentName(aFileName);
   }
 }
diff --git a/base/src/com/google/idea/blaze/base/sync/projectview/LanguageSupport.java b/base/src/com/google/idea/blaze/base/sync/projectview/LanguageSupport.java
index 8e5b39f..b01fbdc 100644
--- a/base/src/com/google/idea/blaze/base/sync/projectview/LanguageSupport.java
+++ b/base/src/com/google/idea/blaze/base/sync/projectview/LanguageSupport.java
@@ -101,7 +101,7 @@
   }
 
   /** @return The set of {@link LanguageClass}'s supported for this {@link WorkspaceType}s. */
-  private static Set<LanguageClass> supportedLanguagesForWorkspaceType(WorkspaceType type) {
+  public static Set<LanguageClass> supportedLanguagesForWorkspaceType(WorkspaceType type) {
     Set<LanguageClass> supportedLanguages = EnumSet.noneOf(LanguageClass.class);
     for (BlazeSyncPlugin syncPlugin : BlazeSyncPlugin.EP_NAME.getExtensions()) {
       supportedLanguages.addAll(syncPlugin.getSupportedLanguagesInWorkspace(type));
diff --git a/base/src/com/google/idea/blaze/base/sync/projectview/SourceTestConfig.java b/base/src/com/google/idea/blaze/base/sync/projectview/SourceTestConfig.java
index 018193e..4af730c 100644
--- a/base/src/com/google/idea/blaze/base/sync/projectview/SourceTestConfig.java
+++ b/base/src/com/google/idea/blaze/base/sync/projectview/SourceTestConfig.java
@@ -15,16 +15,46 @@
  */
 package com.google.idea.blaze.base.sync.projectview;
 
+import com.google.common.annotations.VisibleForTesting;
 import com.google.idea.blaze.base.projectview.ProjectViewSet;
 import com.google.idea.blaze.base.projectview.section.Glob;
 import com.google.idea.blaze.base.projectview.section.sections.TestSourceSection;
+import com.intellij.openapi.util.text.StringUtil;
+import java.io.File;
+import java.util.stream.Collectors;
 
 /** Affects the way sources are imported. */
 public class SourceTestConfig {
   private final Glob.GlobSet testSources;
 
   public SourceTestConfig(ProjectViewSet projectViewSet) {
-    this.testSources = new Glob.GlobSet(projectViewSet.listItems(TestSourceSection.KEY));
+    this.testSources =
+        new Glob.GlobSet(
+            projectViewSet
+                .listItems(TestSourceSection.KEY)
+                .stream()
+                .map(SourceTestConfig::modifyGlob)
+                .collect(Collectors.toList()));
+  }
+
+  private static Glob modifyGlob(Glob glob) {
+    return new Glob(modifyPattern(glob.toString()));
+  }
+
+  /**
+   * We modify the glob patterns provided by the user, so that their behavior more closely matches
+   * what is expected.
+   *
+   * <p>Rules:
+   * <li>path/ => path*
+   * <li>path/* => path*
+   * <li>path => path*
+   */
+  @VisibleForTesting
+  static String modifyPattern(String pattern) {
+    pattern = StringUtil.trimEnd(pattern, '*');
+    pattern = StringUtil.trimEnd(pattern, File.separatorChar);
+    return pattern + "*";
   }
 
   /** Returns true if this artifact is a test artifact. */
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 e843332..cb7ca1f 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
@@ -16,6 +16,7 @@
 package com.google.idea.blaze.base.sync.status;
 
 import com.google.idea.blaze.base.scope.BlazeContext;
+import com.google.idea.blaze.base.sync.BlazeSyncParams.SyncMode;
 import com.google.idea.blaze.base.sync.SyncListener;
 import com.intellij.openapi.project.Project;
 
@@ -26,12 +27,13 @@
 public class BlazeSyncStatusListener extends SyncListener.Adapter {
 
   @Override
-  public void onSyncStart(Project project, BlazeContext context) {
+  public void onSyncStart(Project project, BlazeContext context, SyncMode syncMode) {
     BlazeSyncStatusImpl.getImpl(project).syncStarted();
   }
 
   @Override
-  public void afterSync(Project project, BlazeContext context, SyncResult syncResult) {
+  public void afterSync(
+      Project project, BlazeContext context, SyncMode syncMode, SyncResult syncResult) {
     BlazeSyncStatusImpl.getImpl(project).syncEnded(syncResult);
   }
 }
diff --git a/base/src/com/google/idea/blaze/base/targetmaps/SourceToTargetMapImpl.java b/base/src/com/google/idea/blaze/base/targetmaps/SourceToTargetMapImpl.java
index af965aa..5cf22c6 100644
--- a/base/src/com/google/idea/blaze/base/targetmaps/SourceToTargetMapImpl.java
+++ b/base/src/com/google/idea/blaze/base/targetmaps/SourceToTargetMapImpl.java
@@ -26,6 +26,7 @@
 import com.google.idea.blaze.base.projectview.ProjectViewSet;
 import com.google.idea.blaze.base.scope.BlazeContext;
 import com.google.idea.blaze.base.settings.BlazeImportSettings;
+import com.google.idea.blaze.base.sync.BlazeSyncParams.SyncMode;
 import com.google.idea.blaze.base.sync.SyncListener;
 import com.google.idea.blaze.base.sync.data.BlazeProjectDataManager;
 import com.google.idea.blaze.base.sync.workspace.ArtifactLocationDecoder;
@@ -115,6 +116,7 @@
         BlazeImportSettings importSettings,
         ProjectViewSet projectViewSet,
         BlazeProjectData blazeProjectData,
+        SyncMode syncMode,
         SyncResult syncResult) {
       getImpl(project).clearSourceToTargetMap();
     }
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 348d657..2f7a6b5 100644
--- a/base/src/com/google/idea/blaze/base/wizard2/BlazeSelectProjectViewOption.java
+++ b/base/src/com/google/idea/blaze/base/wizard2/BlazeSelectProjectViewOption.java
@@ -20,11 +20,22 @@
 
 /** Provides an option on the "Select .blazeproject" screen */
 public interface BlazeSelectProjectViewOption extends BlazeWizardOption {
+  /** Returns a shared project view to use */
   @Nullable
-  WorkspacePath getSharedProjectView();
+  default WorkspacePath getSharedProjectView() {
+    return null;
+  }
 
+  /** Returns an initial local project view to use */
   @Nullable
-  String getInitialProjectViewText();
+  default String getInitialProjectViewText() {
+    return null;
+  }
+
+  /** Whether to allow the sections to add default values for the project view */
+  default boolean allowAddDefaultProjectViewValues() {
+    return false;
+  }
 
   void commit();
 }
diff --git a/base/src/com/google/idea/blaze/base/wizard2/CopyExternalProjectViewOption.java b/base/src/com/google/idea/blaze/base/wizard2/CopyExternalProjectViewOption.java
index 2d94db8..62db2c5 100644
--- a/base/src/com/google/idea/blaze/base/wizard2/CopyExternalProjectViewOption.java
+++ b/base/src/com/google/idea/blaze/base/wizard2/CopyExternalProjectViewOption.java
@@ -15,7 +15,6 @@
  */
 package com.google.idea.blaze.base.wizard2;
 
-import com.google.idea.blaze.base.model.primitives.WorkspacePath;
 import com.google.idea.blaze.base.projectview.ProjectViewStorageManager;
 import com.google.idea.blaze.base.ui.BlazeValidationResult;
 import com.google.idea.blaze.base.ui.UiUtil;
@@ -91,12 +90,6 @@
 
   @Nullable
   @Override
-  public WorkspacePath getSharedProjectView() {
-    return null;
-  }
-
-  @Nullable
-  @Override
   public String getInitialProjectViewText() {
     try {
       byte[] bytes = Files.readAllBytes(Paths.get(getProjectViewPath()));
diff --git a/base/src/com/google/idea/blaze/base/wizard2/CreateFromScratchProjectViewOption.java b/base/src/com/google/idea/blaze/base/wizard2/CreateFromScratchProjectViewOption.java
index d886175..366e424 100644
--- a/base/src/com/google/idea/blaze/base/wizard2/CreateFromScratchProjectViewOption.java
+++ b/base/src/com/google/idea/blaze/base/wizard2/CreateFromScratchProjectViewOption.java
@@ -15,7 +15,6 @@
  */
 package com.google.idea.blaze.base.wizard2;
 
-import com.google.idea.blaze.base.model.primitives.WorkspacePath;
 import com.google.idea.blaze.base.ui.BlazeValidationResult;
 import javax.annotation.Nullable;
 import javax.swing.JComponent;
@@ -38,17 +37,16 @@
 
   @Nullable
   @Override
-  public WorkspacePath getSharedProjectView() {
-    return null;
-  }
-
-  @Nullable
-  @Override
   public String getInitialProjectViewText() {
     return "";
   }
 
   @Override
+  public boolean allowAddDefaultProjectViewValues() {
+    return true;
+  }
+
+  @Override
   public void commit() {}
 
   @Override
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 b65059b..e2e41d9 100644
--- a/base/src/com/google/idea/blaze/base/wizard2/GenerateFromBuildFileSelectProjectViewOption.java
+++ b/base/src/com/google/idea/blaze/base/wizard2/GenerateFromBuildFileSelectProjectViewOption.java
@@ -104,12 +104,6 @@
 
   @Nullable
   @Override
-  public WorkspacePath getSharedProjectView() {
-    return null;
-  }
-
-  @Nullable
-  @Override
   public String getInitialProjectViewText() {
     WorkspacePathResolver workspacePathResolver =
         builder.getWorkspaceOption().getWorkspacePathResolver();
@@ -119,6 +113,11 @@
   }
 
   @Override
+  public boolean allowAddDefaultProjectViewValues() {
+    return true;
+  }
+
+  @Override
   public void commit() {
     userSettings.put(LAST_WORKSPACE_PATH, getBuildFilePath());
     buildFilePathField.addCurrentTextToHistory();
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 e4803be..c2d47e1 100644
--- a/base/src/com/google/idea/blaze/base/wizard2/ImportFromWorkspaceProjectViewOption.java
+++ b/base/src/com/google/idea/blaze/base/wizard2/ImportFromWorkspaceProjectViewOption.java
@@ -107,12 +107,6 @@
     return new WorkspacePath(getProjectViewPath());
   }
 
-  @Nullable
-  @Override
-  public String getInitialProjectViewText() {
-    return null;
-  }
-
   @Override
   public void commit() {
     userSettings.put(LAST_WORKSPACE_PATH, getProjectViewPath());
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 0b96d06..2b8c058 100644
--- a/base/src/com/google/idea/blaze/base/wizard2/UseExistingBazelWorkspaceOption.java
+++ b/base/src/com/google/idea/blaze/base/wizard2/UseExistingBazelWorkspaceOption.java
@@ -16,18 +16,51 @@
 package com.google.idea.blaze.base.wizard2;
 
 import com.google.idea.blaze.base.bazel.BuildSystemProvider;
+import com.google.idea.blaze.base.model.primitives.WorkspaceRoot;
 import com.google.idea.blaze.base.settings.Blaze.BuildSystem;
 import com.google.idea.blaze.base.sync.workspace.WorkspacePathResolver;
 import com.google.idea.blaze.base.sync.workspace.WorkspacePathResolverImpl;
 import com.google.idea.blaze.base.ui.BlazeValidationResult;
+import com.google.idea.blaze.base.ui.UiUtil;
+import com.intellij.openapi.fileChooser.FileChooserDescriptor;
+import com.intellij.openapi.fileChooser.FileChooserDialog;
+import com.intellij.openapi.fileChooser.FileChooserFactory;
+import com.intellij.openapi.util.IconLoader;
+import com.intellij.openapi.vfs.LocalFileSystem;
+import com.intellij.openapi.vfs.VirtualFile;
+import com.intellij.ui.TextFieldWithHistory;
 import icons.BlazeIcons;
+import java.awt.Dimension;
 import java.io.File;
-import javax.swing.Icon;
+import javax.swing.JButton;
+import javax.swing.JComponent;
+import javax.swing.JLabel;
 
-class UseExistingBazelWorkspaceOption extends UseExistingWorkspaceOption {
+class UseExistingBazelWorkspaceOption implements BlazeSelectWorkspaceOption {
+
+  private final JComponent component;
+  private final TextFieldWithHistory directoryField;
 
   UseExistingBazelWorkspaceOption(BlazeNewProjectBuilder builder) {
-    super(builder, BuildSystem.Bazel);
+    this.directoryField = new TextFieldWithHistory();
+    this.directoryField.setHistory(builder.getWorkspaceHistory(BuildSystem.Bazel));
+    this.directoryField.setHistorySize(BlazeNewProjectBuilder.HISTORY_SIZE);
+    this.directoryField.setText(builder.getLastImportedWorkspace(BuildSystem.Bazel));
+
+    JButton button = new JButton("...");
+    button.addActionListener(action -> this.chooseDirectory());
+    int buttonSize = this.directoryField.getPreferredSize().height;
+    button.setPreferredSize(new Dimension(buttonSize, buttonSize));
+
+    JComponent box =
+        UiUtil.createHorizontalBox(
+            HORIZONTAL_LAYOUT_GAP,
+            getIconComponent(),
+            new JLabel("Workspace:"),
+            this.directoryField,
+            button);
+    UiUtil.setPreferredWidth(box, PREFERRED_COMPONENT_WIDTH);
+    this.component = box;
   }
 
   @Override
@@ -36,21 +69,6 @@
   }
 
   @Override
-  protected boolean isWorkspaceRoot(File file) {
-    return BuildSystemProvider.getWorkspaceRootProvider(BuildSystem.Bazel).isWorkspaceRoot(file);
-  }
-
-  @Override
-  protected BlazeValidationResult validateWorkspaceRoot(File workspaceRoot) {
-    if (!isWorkspaceRoot(workspaceRoot)) {
-      return BlazeValidationResult.failure(
-          "Invalid workspace root: choose a bazel workspace directory "
-              + "(containing a WORKSPACE file)");
-    }
-    return BlazeValidationResult.success();
-  }
-
-  @Override
   public String getOptionName() {
     return "use-existing-bazel-workspace";
   }
@@ -60,18 +78,105 @@
     return "Use existing bazel workspace";
   }
 
+  private static boolean isWorkspaceRoot(File file) {
+    return BuildSystemProvider.getWorkspaceRootProvider(BuildSystem.Bazel).isWorkspaceRoot(file);
+  }
+
+  private static boolean isWorkspaceRoot(VirtualFile file) {
+    return isWorkspaceRoot(new File(file.getPath()));
+  }
+
   @Override
-  protected String getWorkspaceName(File workspaceRoot) {
+  public BuildSystem getBuildSystemForWorkspace() {
+    return BuildSystem.Bazel;
+  }
+
+  @Override
+  public JComponent getUiComponent() {
+    return component;
+  }
+
+  @Override
+  public void commit() throws BlazeProjectCommitException {}
+
+  @Override
+  public WorkspaceRoot getWorkspaceRoot() {
+    return new WorkspaceRoot(new File(getDirectory()));
+  }
+
+  @Override
+  public File getFileBrowserRoot() {
+    return new File(getDirectory());
+  }
+
+  @Override
+  public String getWorkspaceName() {
+    File workspaceRoot = new File(getDirectory());
     return workspaceRoot.getName();
   }
 
   @Override
-  protected String fileChooserDescription() {
-    return "Select the directory of the workspace you want to use.";
+  public BlazeValidationResult validate() {
+    if (getDirectory().isEmpty()) {
+      return BlazeValidationResult.failure("Please select a workspace");
+    }
+    File workspaceRootFile = new File(getDirectory());
+    if (!workspaceRootFile.exists()) {
+      return BlazeValidationResult.failure("Workspace does not exist");
+    }
+    if (!isWorkspaceRoot(workspaceRootFile)) {
+      return BlazeValidationResult.failure(
+          "Invalid workspace root: choose a bazel workspace directory "
+              + "(containing a WORKSPACE file)");
+    }
+    return BlazeValidationResult.success();
   }
 
-  @Override
-  protected Icon getBuildSystemIcon() {
-    return BlazeIcons.BazelLeaf;
+  private String getDirectory() {
+    return directoryField.getText().trim();
+  }
+
+  private void chooseDirectory() {
+    FileChooserDescriptor descriptor =
+        new FileChooserDescriptor(false, true, false, false, false, false) {
+          @Override
+          public boolean isFileSelectable(VirtualFile file) {
+            // Default implementation doesn't filter directories,
+            // we want to make sure only workspace roots are selectable
+            return super.isFileSelectable(file) && isWorkspaceRoot(file);
+          }
+        }.withHideIgnored(false)
+            .withTitle("Select Workspace Root")
+            .withDescription("Select the directory of the workspace you want to use.")
+            .withFileFilter(UseExistingBazelWorkspaceOption::isWorkspaceRoot);
+    FileChooserDialog chooser =
+        FileChooserFactory.getInstance().createFileChooser(descriptor, null, null);
+
+    final VirtualFile[] files;
+    File existingLocation = new File(getDirectory());
+    if (existingLocation.exists()) {
+      VirtualFile toSelect =
+          LocalFileSystem.getInstance().refreshAndFindFileByPath(existingLocation.getPath());
+      files = chooser.choose(null, toSelect);
+    } else {
+      files = chooser.choose(null);
+    }
+    if (files.length == 0) {
+      return;
+    }
+    VirtualFile file = files[0];
+    directoryField.setText(file.getPath());
+  }
+
+  private static JComponent getIconComponent() {
+    JLabel iconPanel =
+        new JLabel(IconLoader.getIconSnapshot(BlazeIcons.BazelLeaf)) {
+          @Override
+          public boolean isEnabled() {
+            return true;
+          }
+        };
+    UiUtil.setPreferredWidth(iconPanel, 16);
+    return iconPanel;
   }
 }
diff --git a/base/src/com/google/idea/blaze/base/wizard2/UseExistingWorkspaceOption.java b/base/src/com/google/idea/blaze/base/wizard2/UseExistingWorkspaceOption.java
deleted file mode 100644
index ed4e227..0000000
--- a/base/src/com/google/idea/blaze/base/wizard2/UseExistingWorkspaceOption.java
+++ /dev/null
@@ -1,205 +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.wizard2;
-
-import com.google.common.base.Joiner;
-import com.google.idea.blaze.base.model.primitives.WorkspaceRoot;
-import com.google.idea.blaze.base.settings.Blaze.BuildSystem;
-import com.google.idea.blaze.base.ui.BlazeValidationResult;
-import com.google.idea.blaze.base.ui.UiUtil;
-import com.google.idea.blaze.base.vcs.BlazeVcsHandler;
-import com.intellij.icons.AllIcons;
-import com.intellij.openapi.fileChooser.FileChooserDescriptor;
-import com.intellij.openapi.fileChooser.FileChooserDialog;
-import com.intellij.openapi.fileChooser.FileChooserFactory;
-import com.intellij.openapi.util.IconLoader;
-import com.intellij.openapi.vfs.LocalFileSystem;
-import com.intellij.openapi.vfs.VirtualFile;
-import com.intellij.ui.TextFieldWithHistory;
-import java.awt.Dimension;
-import java.io.File;
-import java.util.Arrays;
-import java.util.stream.Collectors;
-import javax.swing.Icon;
-import javax.swing.JButton;
-import javax.swing.JComponent;
-import javax.swing.JLabel;
-
-/** Option to use an existing workspace */
-public abstract class UseExistingWorkspaceOption implements BlazeSelectWorkspaceOption {
-
-  private final JComponent component;
-  private final TextFieldWithHistory directoryField;
-  private final BuildSystem buildSystem;
-
-  protected UseExistingWorkspaceOption(BlazeNewProjectBuilder builder, BuildSystem buildSystem) {
-    this.buildSystem = buildSystem;
-
-    this.directoryField = new TextFieldWithHistory();
-    directoryField.setHistory(builder.getWorkspaceHistory(buildSystem));
-    directoryField.setHistorySize(BlazeNewProjectBuilder.HISTORY_SIZE);
-    directoryField.setText(builder.getLastImportedWorkspace(buildSystem));
-
-    JButton button = new JButton("...");
-    button.addActionListener(action -> chooseDirectory());
-    int buttonSize = directoryField.getPreferredSize().height;
-    button.setPreferredSize(new Dimension(buttonSize, buttonSize));
-
-    JComponent box =
-        UiUtil.createHorizontalBox(
-            HORIZONTAL_LAYOUT_GAP,
-            getIconComponent(),
-            new JLabel("Workspace:"),
-            directoryField,
-            button);
-    UiUtil.setPreferredWidth(box, PREFERRED_COMPONENT_WIDTH);
-    this.component = box;
-  }
-
-  protected abstract boolean isWorkspaceRoot(File file);
-
-  protected abstract BlazeValidationResult validateWorkspaceRoot(File workspaceRoot);
-
-  private boolean isWorkspaceRoot(VirtualFile file) {
-    return isWorkspaceRoot(new File(file.getPath()));
-  }
-
-  protected abstract String fileChooserDescription();
-
-  protected abstract Icon getBuildSystemIcon();
-
-  protected abstract String getWorkspaceName(File workspaceRoot);
-
-  @Override
-  public BuildSystem getBuildSystemForWorkspace() {
-    return buildSystem;
-  }
-
-  @Override
-  public JComponent getUiComponent() {
-    return component;
-  }
-
-  @Override
-  public void commit() throws BlazeProjectCommitException {}
-
-  @Override
-  public WorkspaceRoot getWorkspaceRoot() {
-    return new WorkspaceRoot(new File(getDirectory()));
-  }
-
-  @Override
-  public File getFileBrowserRoot() {
-    return new File(getDirectory());
-  }
-
-  @Override
-  public String getWorkspaceName() {
-    File workspaceRoot = new File(getDirectory());
-    return getWorkspaceName(workspaceRoot);
-  }
-
-  @Override
-  public BlazeValidationResult validate() {
-    if (getDirectory().isEmpty()) {
-      return BlazeValidationResult.failure("Please select a workspace");
-    }
-
-    File workspaceRootFile = new File(getDirectory());
-    if (!workspaceRootFile.exists()) {
-      return BlazeValidationResult.failure("Workspace does not exist");
-    }
-
-    WorkspaceRoot workspaceRoot = new WorkspaceRoot(workspaceRootFile);
-    boolean hasVcsHandler =
-        Arrays.stream(BlazeVcsHandler.EP_NAME.getExtensions())
-            .anyMatch(vcsHandler -> vcsHandler.handlesProject(buildSystem, workspaceRoot));
-    if (!hasVcsHandler) {
-      StringBuilder sb = new StringBuilder();
-      sb.append("Workspace is not of a supported VCS type. ");
-      sb.append("VCS types considered were: ");
-      Joiner.on(", ")
-          .appendTo(
-              sb,
-              Arrays.stream(BlazeVcsHandler.EP_NAME.getExtensions())
-                  .map(BlazeVcsHandler::getVcsName)
-                  .collect(Collectors.toList()));
-      return BlazeValidationResult.failure(sb.toString());
-    }
-
-    return validateWorkspaceRoot(workspaceRootFile);
-  }
-
-  private String getDirectory() {
-    return directoryField.getText().trim();
-  }
-
-  private void chooseDirectory() {
-    FileChooserDescriptor descriptor =
-        new FileChooserDescriptor(false, true, false, false, false, false) {
-          @Override
-          public boolean isFileSelectable(VirtualFile file) {
-            // Default implementation doesn't filter directories,
-            // we want to make sure only workspace roots are selectable
-            return super.isFileSelectable(file) && isWorkspaceRoot(file);
-          }
-
-          @Override
-          public Icon getIcon(VirtualFile file) {
-            if (buildSystem == BuildSystem.Bazel) {
-              // isWorkspaceRoot requires file system calls -- it's too expensive
-              return super.getIcon(file);
-            }
-            if (isWorkspaceRoot(file)) {
-              return AllIcons.Nodes.SourceFolder;
-            }
-            return super.getIcon(file);
-          }
-        }.withHideIgnored(false)
-            .withTitle("Select Workspace Root")
-            .withDescription(fileChooserDescription())
-            .withFileFilter(this::isWorkspaceRoot);
-    FileChooserDialog chooser =
-        FileChooserFactory.getInstance().createFileChooser(descriptor, null, null);
-
-    final VirtualFile[] files;
-    File existingLocation = new File(getDirectory());
-    if (existingLocation.exists()) {
-      VirtualFile toSelect =
-          LocalFileSystem.getInstance().refreshAndFindFileByPath(existingLocation.getPath());
-      files = chooser.choose(null, toSelect);
-    } else {
-      files = chooser.choose(null);
-    }
-    if (files.length == 0) {
-      return;
-    }
-    VirtualFile file = files[0];
-    directoryField.setText(file.getPath());
-  }
-
-  private JComponent getIconComponent() {
-    JLabel iconPanel =
-        new JLabel(IconLoader.getIconSnapshot(getBuildSystemIcon())) {
-          @Override
-          public boolean isEnabled() {
-            return true;
-          }
-        };
-    UiUtil.setPreferredWidth(iconPanel, 16);
-    return iconPanel;
-  }
-}
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 594d652..0307b5a 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
@@ -22,10 +22,14 @@
 import com.google.idea.blaze.base.model.primitives.WorkspaceRoot;
 import com.google.idea.blaze.base.projectview.ProjectView;
 import com.google.idea.blaze.base.projectview.ProjectViewSet;
+import com.google.idea.blaze.base.projectview.ProjectViewSet.ProjectViewFile;
 import com.google.idea.blaze.base.projectview.ProjectViewStorageManager;
 import com.google.idea.blaze.base.projectview.ProjectViewVerifier;
+import com.google.idea.blaze.base.projectview.parser.ProjectViewParser;
 import com.google.idea.blaze.base.projectview.section.ScalarSection;
+import com.google.idea.blaze.base.projectview.section.SectionParser;
 import com.google.idea.blaze.base.projectview.section.sections.ImportSection;
+import com.google.idea.blaze.base.projectview.section.sections.Sections;
 import com.google.idea.blaze.base.scope.BlazeContext;
 import com.google.idea.blaze.base.scope.OutputSink.Propagation;
 import com.google.idea.blaze.base.scope.Scope;
@@ -44,6 +48,7 @@
 import com.google.idea.blaze.base.wizard2.BlazeSelectProjectViewOption;
 import com.google.idea.blaze.base.wizard2.BlazeSelectWorkspaceOption;
 import com.google.idea.blaze.base.wizard2.ProjectDataDirectoryValidator;
+import com.google.idea.common.experiments.BoolExperiment;
 import com.intellij.ide.RecentProjectsManager;
 import com.intellij.openapi.Disposable;
 import com.intellij.openapi.application.ApplicationNamesInfo;
@@ -76,6 +81,9 @@
       new FileChooserDescriptor(false, true, false, false, false, false);
   private static final Logger LOG = Logger.getInstance(BlazeEditProjectViewControl.class);
 
+  private static final BoolExperiment allowAddprojectViewDefaultValues =
+      new BoolExperiment("allow.add.project.view.default.values", true);
+
   private final JPanel component;
   private final String buildSystemName;
   private final ProjectViewUi projectViewUi;
@@ -143,6 +151,10 @@
     WorkspaceRoot workspaceRoot = workspaceOption.getWorkspaceRoot();
     WorkspacePath workspacePath = projectViewOption.getSharedProjectView();
     String initialProjectViewText = projectViewOption.getInitialProjectViewText();
+    boolean allowAddDefaultValues =
+        projectViewOption.allowAddDefaultProjectViewValues()
+            && allowAddprojectViewDefaultValues.getValue();
+    WorkspacePathResolver workspacePathResolver = workspaceOption.getWorkspacePathResolver();
 
     HashCode hashCode =
         Hashing.md5()
@@ -151,6 +163,7 @@
             .putUnencodedChars(workspaceRoot.toString())
             .putUnencodedChars(workspacePath != null ? workspacePath.toString() : "")
             .putUnencodedChars(initialProjectViewText != null ? initialProjectViewText : "")
+            .putBoolean(allowAddDefaultValues)
             .hash();
 
     // If any params have changed, reinit the control
@@ -159,18 +172,42 @@
       init(
           workspaceName,
           workspaceRoot,
-          workspaceOption.getWorkspacePathResolver(),
+          workspacePathResolver,
           workspacePath,
-          initialProjectViewText);
+          initialProjectViewText,
+          allowAddDefaultValues);
     }
   }
 
+  private static String modifyInitialProjectView(
+      String initialProjectViewText, WorkspacePathResolver workspacePathResolver) {
+    BlazeContext context = new BlazeContext();
+    ProjectViewParser projectViewParser = new ProjectViewParser(context, workspacePathResolver);
+    projectViewParser.parseProjectView(initialProjectViewText);
+    ProjectViewSet projectViewSet = projectViewParser.getResult();
+    ProjectViewFile projectViewFile = projectViewSet.getTopLevelProjectViewFile();
+    if (projectViewFile == null) {
+      return initialProjectViewText;
+    }
+    ProjectView projectView = projectViewFile.projectView;
+    for (SectionParser sectionParser : Sections.getParsers()) {
+      projectView = sectionParser.addProjectViewDefaultValue(projectView);
+    }
+    return ProjectViewParser.projectViewToString(projectView);
+  }
+
   private void init(
       String workspaceName,
       WorkspaceRoot workspaceRoot,
       WorkspacePathResolver workspacePathResolver,
       @Nullable WorkspacePath sharedProjectView,
-      @Nullable String initialProjectViewText) {
+      @Nullable String initialProjectViewText,
+      boolean allowAddDefaultValues) {
+    if (allowAddDefaultValues && initialProjectViewText != null) {
+      initialProjectViewText =
+          modifyInitialProjectView(initialProjectViewText, workspacePathResolver);
+    }
+
     this.workspaceRoot = workspaceRoot;
     projectNameField.setText(workspaceName);
     String defaultDataDir = getDefaultProjectDataDirectory(workspaceName);
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 aeff26f..d91307f 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
@@ -54,10 +54,10 @@
                   new WorkspacePath("BUILD"),
                   "def function(name, deps, srcs):",
                   "  # empty function",
-                  "function(d");
+                  "function(dep");
 
           Editor editor = editorTest.openFileInEditor(file.getVirtualFile());
-          editorTest.setCaretPosition(editor, 2, "function(n".length());
+          editorTest.setCaretPosition(editor, 2, "function(dep".length());
 
           LookupElement[] completionItems = testFixture.completeBasic();
           assertThat(completionItems).isNull();
@@ -82,7 +82,6 @@
           editorTest.setCaretPosition(editor, 2, "function(".length());
 
           String[] completionItems = editorTest.getCompletionItemsAsStrings();
-          assertThat(completionItems).hasLength(4);
           assertThat(completionItems).asList().containsAllOf("name", "deps", "srcs", "function");
         });
   }
diff --git a/base/tests/integrationtests/com/google/idea/blaze/base/lang/buildfile/completion/BuildLabelAutoCompletionTest.java b/base/tests/integrationtests/com/google/idea/blaze/base/lang/buildfile/completion/BuildLabelAutoCompletionTest.java
new file mode 100644
index 0000000..bfeb7aa
--- /dev/null
+++ b/base/tests/integrationtests/com/google/idea/blaze/base/lang/buildfile/completion/BuildLabelAutoCompletionTest.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.lang.buildfile.completion;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.google.common.collect.ImmutableList;
+import com.google.idea.blaze.base.lang.buildfile.BuildFileIntegrationTestCase;
+import com.google.idea.blaze.base.lang.buildfile.psi.BuildFile;
+import com.google.idea.blaze.base.model.primitives.WorkspacePath;
+import com.intellij.codeInsight.lookup.LookupElement;
+import com.intellij.codeInsight.lookup.impl.LookupImpl;
+import com.intellij.openapi.editor.Editor;
+import com.intellij.testFramework.fixtures.CompletionAutoPopupTester;
+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 auto-popup code completion in BUILD labels. */
+@RunWith(JUnit4.class)
+public class BuildLabelAutoCompletionTest extends BuildFileIntegrationTestCase {
+
+  private CompletionAutoPopupTester completionTester;
+
+  @Before
+  public final void before() {
+    completionTester = new CompletionAutoPopupTester(testFixture);
+  }
+
+  /** Completion UI testing can't be run on the EDT. */
+  @Override
+  protected boolean runTestsOnEdt() {
+    return false;
+  }
+
+  @Test
+  public void testPopupAutocompleteAfterSlash() {
+    completionTester.runWithAutoPopupEnabled(
+        () -> {
+          createBuildFile(new WorkspacePath("java/com/foo/BUILD"));
+          BuildFile file =
+              createBuildFile(
+                  new WorkspacePath("BUILD"),
+                  "java_library(",
+                  "    name = 'lib',",
+                  "    srcs = [''],");
+
+          Editor editor = editorTest.openFileInEditor(file.getVirtualFile());
+          editorTest.setCaretPosition(editor, 2, "    srcs = ['".length());
+
+          completionTester.typeWithPauses("/");
+          assertThat(currentLookupStrings()).containsExactly("'//java/com/foo'");
+        });
+  }
+
+  @Test
+  public void testPopupAutocompleteAfterColon() {
+    completionTester.runWithAutoPopupEnabled(
+        () -> {
+          createBuildFile(new WorkspacePath("java/com/foo/BUILD"), "java_library(name = 'target')");
+          BuildFile file =
+              createBuildFile(
+                  new WorkspacePath("BUILD"),
+                  "java_library(",
+                  "    name = 'lib',",
+                  "    srcs = ['//java/com/foo'],");
+
+          Editor editor = editorTest.openFileInEditor(file.getVirtualFile());
+          editorTest.setCaretPosition(editor, 2, "    srcs = ['//java/com/foo".length());
+
+          completionTester.typeWithPauses(":");
+          assertThat(currentLookupStrings()).containsExactly("'//java/com/foo:target'");
+        });
+  }
+
+  @Test
+  public void testPopupAutocompleteAfterLetter() {
+    // test for IntelliJ's standard autocomplete popup trigger
+    completionTester.runWithAutoPopupEnabled(
+        () -> {
+          createBuildFile(new WorkspacePath("java/com/foo/BUILD"), "java_library(name = 'target')");
+          BuildFile file =
+              createBuildFile(
+                  new WorkspacePath("BUILD"),
+                  "java_library(",
+                  "    name = 'lib',",
+                  "    srcs = ['//'],");
+
+          Editor editor = editorTest.openFileInEditor(file.getVirtualFile());
+          editorTest.setCaretPosition(editor, 2, "    srcs = ['//".length());
+
+          completionTester.typeWithPauses("j");
+          assertThat(currentLookupStrings()).containsExactly("'//java/com/foo'");
+        });
+  }
+
+  private List<String> currentLookupStrings() {
+    LookupImpl lookup = completionTester.getLookup();
+    if (lookup == null) {
+      return ImmutableList.of();
+    }
+    return lookup
+        .getItems()
+        .stream()
+        .map(LookupElement::getLookupString)
+        .collect(Collectors.toList());
+  }
+}
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 a6c6b9e..8199591 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
@@ -25,6 +25,7 @@
 import com.google.idea.blaze.base.lang.buildfile.language.semantics.RuleDefinition;
 import com.google.idea.blaze.base.lang.buildfile.psi.BuildFile;
 import com.google.idea.blaze.base.model.primitives.WorkspacePath;
+import com.google.idea.testing.ServiceHelper;
 import com.google.repackaged.devtools.build.lib.query2.proto.proto2api.Build;
 import com.intellij.codeInsight.lookup.LookupElement;
 import com.intellij.openapi.editor.Editor;
@@ -35,7 +36,7 @@
 import org.junit.runner.RunWith;
 import org.junit.runners.JUnit4;
 
-/** Tests for BuiltInFunctionAttributeCompletionContributor. */
+/** Tests for {@link BuiltInFunctionAttributeCompletionContributor}. */
 @RunWith(JUnit4.class)
 public class BuiltInFunctionAttributeCompletionContributorTest
     extends BuildFileIntegrationTestCase {
@@ -65,10 +66,10 @@
   public void testSimpleSingleCompletion() {
     setRuleAndAttributes("sh_binary", "name", "deps", "srcs", "data");
 
-    BuildFile file = createBuildFile(new WorkspacePath("BUILD"), "sh_binary(", "    n");
+    BuildFile file = createBuildFile(new WorkspacePath("BUILD"), "sh_binary(", "    nam");
 
     Editor editor = editorTest.openFileInEditor(file.getVirtualFile());
-    editorTest.setCaretPosition(editor, 1, "    n".length());
+    editorTest.setCaretPosition(editor, 1, "    nam".length());
 
     String[] completionItems = editorTest.getCompletionItemsAsStrings();
     assertThat(completionItems).isNull();
@@ -77,6 +78,15 @@
 
   @Test
   public void testNoCompletionInUnknownRule() {
+    ServiceHelper.unregisterLanguageExtensionPoint(
+        "com.intellij.completion.contributor",
+        BuiltInSymbolCompletionContributor.class,
+        getTestRootDisposable());
+    ServiceHelper.unregisterLanguageExtensionPoint(
+        "com.intellij.completion.contributor",
+        BuiltInFunctionCompletionContributor.class,
+        getTestRootDisposable());
+
     setRuleAndAttributes("sh_binary", "name", "deps", "srcs", "data");
 
     BuildFile file = createBuildFile(new WorkspacePath("BUILD"), "java_binary(");
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 6402fc2..6b19f7b 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
@@ -24,6 +24,7 @@
 import com.google.idea.blaze.base.lang.buildfile.language.semantics.RuleDefinition;
 import com.google.idea.blaze.base.lang.buildfile.psi.BuildFile;
 import com.google.idea.blaze.base.model.primitives.WorkspacePath;
+import com.google.idea.testing.ServiceHelper;
 import com.intellij.codeInsight.lookup.LookupElement;
 import com.intellij.openapi.editor.Editor;
 import com.intellij.openapi.project.Project;
@@ -33,7 +34,7 @@
 import org.junit.runner.RunWith;
 import org.junit.runners.JUnit4;
 
-/** Tests BuiltInFunctionCompletionContributor */
+/** Tests {@link BuiltInFunctionCompletionContributor} */
 @RunWith(JUnit4.class)
 public class BuiltInFunctionCompletionContributorTest extends BuildFileIntegrationTestCase {
 
@@ -54,11 +55,8 @@
     Editor editor = editorTest.openFileInEditor(file.getVirtualFile());
     editorTest.setCaretPosition(editor, 0, 0);
 
-    LookupElement[] completionItems = testFixture.completeBasic();
-    assertThat(completionItems).hasLength(2);
-    assertThat(completionItems[0].getLookupString()).isEqualTo("android_binary");
-    assertThat(completionItems[1].getLookupString()).isEqualTo("java_library");
-
+    String[] completionItems = editorTest.getCompletionItemsAsStrings();
+    assertThat(completionItems).asList().containsAllOf("android_binary", "java_library");
     assertFileContents(file, "");
   }
 
@@ -66,10 +64,10 @@
   public void testUniqueTopLevelCompletion() {
     setRules("java_library", "android_binary");
 
-    BuildFile file = createBuildFile(new WorkspacePath("BUILD"), "ja");
+    BuildFile file = createBuildFile(new WorkspacePath("BUILD"), "java_libra");
 
     Editor editor = editorTest.openFileInEditor(file.getVirtualFile());
-    editorTest.setCaretPosition(editor, 0, 2);
+    editorTest.setCaretPosition(editor, 0, "java_libra".length());
 
     LookupElement[] completionItems = testFixture.completeBasic();
     assertThat(completionItems).isNull();
@@ -83,10 +81,11 @@
     setRules("java_library", "android_binary");
 
     BuildFile file =
-        createBuildFile(new WorkspacePath("build_defs.bzl"), "def function():", "  native.j");
+        createBuildFile(
+            new WorkspacePath("build_defs.bzl"), "def function():", "  native.java_libra");
 
     Editor editor = editorTest.openFileInEditor(file.getVirtualFile());
-    editorTest.setCaretPosition(editor, 1, "  native.j".length());
+    editorTest.setCaretPosition(editor, 1, "  native.java_libra".length());
 
     LookupElement[] completionItems = testFixture.completeBasic();
     assertThat(completionItems).isNull();
@@ -97,6 +96,11 @@
 
   @Test
   public void testNoCompletionInsideRule() {
+    ServiceHelper.unregisterLanguageExtensionPoint(
+        "com.intellij.completion.contributor",
+        BuiltInSymbolCompletionContributor.class,
+        getTestRootDisposable());
+
     setRules("java_library", "android_binary");
 
     String[] contents = {"java_library(", "    name = \"lib\"", ""};
@@ -123,6 +127,20 @@
     assertThat(editorTest.getCompletionItemsAsStrings()).isEmpty();
   }
 
+  @Test
+  public void testGlobalFunctions() {
+    BuildFile file = createBuildFile(new WorkspacePath("BUILD"), "licen");
+
+    Editor editor = editorTest.openFileInEditor(file.getVirtualFile());
+    editorTest.setCaretPosition(editor, 0, 5);
+
+    LookupElement[] completionItems = testFixture.completeBasic();
+    assertThat(completionItems).isNull();
+
+    assertFileContents(file, "licenses()");
+    editorTest.assertCaretPosition(editor, 0, "licenses(".length());
+  }
+
   private void setRules(String... ruleNames) {
     ImmutableMap.Builder<String, RuleDefinition> rules = ImmutableMap.builder();
     for (String name : ruleNames) {
@@ -133,7 +151,7 @@
 
   private static class MockBuildLanguageSpecProvider implements BuildLanguageSpecProvider {
 
-    BuildLanguageSpec languageSpec;
+    BuildLanguageSpec languageSpec = new BuildLanguageSpec(ImmutableMap.of());
 
     void setRules(ImmutableMap<String, RuleDefinition> rules) {
       languageSpec = new BuildLanguageSpec(rules);
diff --git a/base/tests/integrationtests/com/google/idea/blaze/base/lang/buildfile/completion/BuiltInSymbolCompletionContributorTest.java b/base/tests/integrationtests/com/google/idea/blaze/base/lang/buildfile/completion/BuiltInSymbolCompletionContributorTest.java
new file mode 100644
index 0000000..c48cfd5
--- /dev/null
+++ b/base/tests/integrationtests/com/google/idea/blaze/base/lang/buildfile/completion/BuiltInSymbolCompletionContributorTest.java
@@ -0,0 +1,86 @@
+/*
+ * Copyright 2016 The Bazel Authors. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.idea.blaze.base.lang.buildfile.completion;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.google.idea.blaze.base.lang.buildfile.BuildFileIntegrationTestCase;
+import com.google.idea.blaze.base.lang.buildfile.language.semantics.BuildLanguageSpec;
+import com.google.idea.blaze.base.lang.buildfile.language.semantics.BuildLanguageSpecProvider;
+import com.google.idea.blaze.base.lang.buildfile.psi.BuildFile;
+import com.google.idea.blaze.base.model.primitives.WorkspacePath;
+import com.intellij.codeInsight.lookup.LookupElement;
+import com.intellij.openapi.editor.Editor;
+import com.intellij.openapi.project.Project;
+import javax.annotation.Nullable;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** Tests {@link BuiltInSymbolCompletionContributor} */
+@RunWith(JUnit4.class)
+public class BuiltInSymbolCompletionContributorTest extends BuildFileIntegrationTestCase {
+
+  @Before
+  public final void before() {
+    registerApplicationService(
+        BuildLanguageSpecProvider.class,
+        new BuildLanguageSpecProvider() {
+          @Nullable
+          @Override
+          public BuildLanguageSpec getLanguageSpec(Project project) {
+            return null;
+          }
+        });
+  }
+
+  @Test
+  public void testSimpleTopLevelCompletion() {
+    BuildFile file = createBuildFile(new WorkspacePath("BUILD"), "");
+
+    Editor editor = editorTest.openFileInEditor(file.getVirtualFile());
+    editorTest.setCaretPosition(editor, 0, 0);
+
+    String[] completionItems = editorTest.getCompletionItemsAsStrings();
+    assertThat(completionItems).asList().containsAnyOf("PACKAGE_NAME", "len", "dict", "struct");
+    assertFileContents(file, "");
+  }
+
+  @Test
+  public void testUniqueTopLevelCompletion() {
+    BuildFile file = createBuildFile(new WorkspacePath("BUILD"), "PACKAGE_N");
+
+    Editor editor = editorTest.openFileInEditor(file.getVirtualFile());
+    editorTest.setCaretPosition(editor, 0, "PACKAGE_N".length());
+
+    LookupElement[] completionItems = testFixture.completeBasic();
+    assertThat(completionItems).isNull();
+
+    assertFileContents(file, "PACKAGE_NAME");
+    editorTest.assertCaretPosition(editor, 0, "PACKAGE_NAME".length());
+  }
+
+  @Test
+  public void testNoCompletionInComment() {
+    BuildFile file = createBuildFile(new WorkspacePath("BUILD"), "#PACK");
+
+    Editor editor = editorTest.openFileInEditor(file.getVirtualFile());
+    editorTest.setCaretPosition(editor, 0, "#PACK".length());
+
+    assertThat(editorTest.getCompletionItemsAsStrings()).isEmpty();
+  }
+}
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 d29676a..a4147d8 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
@@ -39,11 +39,11 @@
 
   @Test
   public void testLocalVariable() {
-    setInput("var = [a, b]", "def function(name, deps, srcs):", "  v<caret>");
+    setInput("var_name = [a, b]", "def function(name, deps, srcs):", "  var_n<caret>");
 
     editorTest.completeIfUnique();
 
-    assertResult("var = [a, b]", "def function(name, deps, srcs):", "  var<caret>");
+    assertResult("var_name = [a, b]", "def function(name, deps, srcs):", "  var_name<caret>");
   }
 
   @Test
@@ -57,7 +57,7 @@
 
   @Test
   public void testNoCompletionAfterDot() {
-    setInput("var = [a, b]", "def function(name, deps, srcs):", "  ext.v<caret>");
+    setInput("var_name = [a, b]", "def function(name, deps, srcs):", "  ext.var_na<caret>");
 
     String[] completionItems = editorTest.getCompletionItemsAsStrings();
     assertThat(completionItems).isEmpty();
@@ -65,41 +65,41 @@
 
   @Test
   public void testFunctionParam() {
-    setInput("def test(var):", "  v<caret>");
+    setInput("def test(var_name):", "  var_na<caret>");
 
     editorTest.completeIfUnique();
 
-    assertResult("def test(var):", "  var<caret>");
+    assertResult("def test(var_name):", "  var_name<caret>");
   }
 
   // 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>");
+    setInput("var_name = 1", "var_name = 2", "var_name = 3", "var_na<caret>");
 
     editorTest.completeIfUnique();
 
-    assertResult("var = 1", "var = 2", "var = 3", "var<caret>");
+    assertResult("var_name = 1", "var_name = 2", "var_name = 3", "var_name<caret>");
   }
 
   @Test
   public void testSymbolDefinedOutsideScope() {
-    setInput("<caret>", "var = 1");
+    setInput("var_na<caret>", "var_name = 1");
 
     assertThat(editorTest.getCompletionItemsAsStrings()).isEmpty();
   }
 
   @Test
   public void testSymbolDefinedOutsideScope2() {
-    setInput("def fn():", "  var = 1", "v<caret>");
+    setInput("def fn():", "  var_name = 1", "var_na<caret>");
 
     assertThat(testFixture.completeBasic()).isEmpty();
   }
 
   @Test
   public void testSymbolDefinedOutsideScope3() {
-    setInput("for var in (1, 2, 3): print var", "v<caret>");
+    setInput("for var_name in (1, 2, 3): print var_name", "var_na<caret>");
 
     assertThat(testFixture.completeBasic()).isEmpty();
   }
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 b7a8392..f50dbf2 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
@@ -57,6 +57,7 @@
 
     Argument.Keyword arg =
         buildFile.findChildByClass(FuncallExpression.class).getKeywordArgument("srcs");
+    assertThat(arg).isNotNull();
 
     PsiElement ref = references[0].getElement();
     assertThat(ref).isInstanceOf(StringLiteral.class);
diff --git a/base/tests/unittests/com/google/idea/blaze/base/issueparser/BlazeIssueParserTest.java b/base/tests/unittests/com/google/idea/blaze/base/issueparser/BlazeIssueParserTest.java
index 322754d..437822a 100644
--- a/base/tests/unittests/com/google/idea/blaze/base/issueparser/BlazeIssueParserTest.java
+++ b/base/tests/unittests/com/google/idea/blaze/base/issueparser/BlazeIssueParserTest.java
@@ -21,6 +21,7 @@
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.when;
 
+import com.google.common.collect.ImmutableList;
 import com.google.idea.blaze.base.BlazeTestCase;
 import com.google.idea.blaze.base.model.primitives.TargetExpression;
 import com.google.idea.blaze.base.model.primitives.WorkspaceRoot;
@@ -30,10 +31,11 @@
 import com.google.idea.blaze.base.projectview.section.ListSection;
 import com.google.idea.blaze.base.projectview.section.sections.TargetSection;
 import com.google.idea.blaze.base.scope.output.IssueOutput;
+import com.google.idea.blaze.base.scope.output.IssueOutput.Category;
 import com.google.idea.common.experiments.ExperimentService;
 import com.google.idea.common.experiments.MockExperimentService;
 import java.io.File;
-import org.jetbrains.annotations.NotNull;
+import java.util.regex.Matcher;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.junit.runners.JUnit4;
@@ -44,10 +46,10 @@
 
   private ProjectViewManager projectViewManager;
   private WorkspaceRoot workspaceRoot;
+  private ImmutableList<BlazeIssueParser.Parser> parsers;
 
   @Override
-  protected void initTest(
-      @NotNull Container applicationServices, @NotNull Container projectServices) {
+  protected void initTest(Container applicationServices, Container projectServices) {
     super.initTest(applicationServices, projectServices);
 
     applicationServices.register(ExperimentService.class, new MockExperimentService());
@@ -55,12 +57,40 @@
     projectViewManager = mock(ProjectViewManager.class);
     projectServices.register(ProjectViewManager.class, projectViewManager);
 
+    ProjectViewSet projectViewSet =
+        ProjectViewSet.builder()
+            .add(
+                new File(".blazeproject"),
+                ProjectView.builder()
+                    .add(
+                        ListSection.builder(TargetSection.KEY)
+                            .add(TargetExpression.fromString("//tests/com/google/a/b/c/d/baz:baz"))
+                            .add(TargetExpression.fromString("//package/path:hello4")))
+                    .build())
+            .build();
+    when(projectViewManager.getProjectViewSet()).thenReturn(projectViewSet);
+
     workspaceRoot = new WorkspaceRoot(new File("/root"));
+
+    parsers =
+        ImmutableList.of(
+            new BlazeIssueParser.CompileParser(workspaceRoot),
+            new BlazeIssueParser.TracebackParser(),
+            new BlazeIssueParser.BuildParser(),
+            new BlazeIssueParser.LinelessBuildParser(),
+            new BlazeIssueParser.ProjectViewLabelParser(projectViewSet),
+            new BlazeIssueParser.InvalidTargetProjectViewPackageParser(
+                projectViewSet, "no such package '(.*)': BUILD file not found on package path"),
+            new BlazeIssueParser.InvalidTargetProjectViewPackageParser(
+                projectViewSet, "no targets found beneath '(.*)'"),
+            new BlazeIssueParser.InvalidTargetProjectViewPackageParser(
+                projectViewSet, "ERROR: invalid target format '(.*)'"),
+            new BlazeIssueParser.FileNotFoundBuildParser(workspaceRoot));
   }
 
   @Test
   public void testParseTargetError() {
-    BlazeIssueParser blazeIssueParser = new BlazeIssueParser(project, workspaceRoot);
+    BlazeIssueParser blazeIssueParser = new BlazeIssueParser(parsers);
     IssueOutput issue =
         blazeIssueParser.parseIssue(
             "ERROR: invalid target format "
@@ -74,7 +104,7 @@
 
   @Test
   public void testParseCompileError() {
-    BlazeIssueParser blazeIssueParser = new BlazeIssueParser(project, workspaceRoot);
+    BlazeIssueParser blazeIssueParser = new BlazeIssueParser(parsers);
     IssueOutput issue =
         blazeIssueParser.parseIssue(
             "java/com/google/android/samples/helloroot/math/DivideMath.java:17: error: "
@@ -83,6 +113,7 @@
     assertThat(issue.getFile().getPath())
         .isEqualTo("/root/java/com/google/android/samples/helloroot/math/DivideMath.java");
     assertThat(issue.getLine()).isEqualTo(17);
+    assertThat(issue.getColumn()).isEqualTo(-1);
     assertThat(issue.getMessage())
         .isEqualTo("non-static variable this cannot be referenced from a static context");
     assertThat(issue.getCategory()).isEqualTo(IssueOutput.Category.ERROR);
@@ -90,50 +121,29 @@
 
   @Test
   public void testParseCompileErrorWithColumn() {
-    BlazeIssueParser blazeIssueParser = new BlazeIssueParser(project, workspaceRoot);
+    BlazeIssueParser blazeIssueParser = new BlazeIssueParser(parsers);
     IssueOutput issue =
         blazeIssueParser.parseIssue(
             "java/com/google/devtools/aswb/pluginrepo/googleplex/PluginsEndpoint.java:33:26: "
                 + "error: '|' is not preceded with whitespace.");
     assertNotNull(issue);
     assertThat(issue.getLine()).isEqualTo(33);
+    assertThat(issue.getColumn()).isEqualTo(26);
     assertThat(issue.getMessage()).isEqualTo("'|' is not preceded with whitespace.");
     assertThat(issue.getCategory()).isEqualTo(IssueOutput.Category.ERROR);
   }
 
   @Test
-  public void testParseCompileErrorWithAbsolutePath() {
-    BlazeIssueParser blazeIssueParser = new BlazeIssueParser(project, workspaceRoot);
-    IssueOutput issue =
-        blazeIssueParser.parseIssue(
-            "/root/java/com/google/android/samples/helloroot/math/DivideMath.java:17: error: "
-                + "non-static variable this cannot be referenced from a static context");
-    assertNotNull(issue);
-    assertThat(issue.getFile().getPath())
-        .isEqualTo("/root/java/com/google/android/samples/helloroot/math/DivideMath.java");
-  }
-
-  @Test
-  public void testParseCompileErrorWithDepotPath() {
-    BlazeIssueParser blazeIssueParser = new BlazeIssueParser(project, workspaceRoot);
-    IssueOutput issue =
-        blazeIssueParser.parseIssue(
-            "//depot/google3/package_path/DivideMath.java:17: error: "
-                + "non-static variable this cannot be referenced from a static context");
-    assertNotNull(issue);
-    assertThat(issue.getFile().getPath()).isEqualTo("/root/package_path/DivideMath.java");
-  }
-
-  @Test
   public void testParseBuildError() {
-    BlazeIssueParser blazeIssueParser = new BlazeIssueParser(project, workspaceRoot);
+    BlazeIssueParser blazeIssueParser = new BlazeIssueParser(parsers);
     IssueOutput issue =
         blazeIssueParser.parseIssue(
-            "ERROR: /path/to/root/javatests/package_path/BUILD:42:12: "
+            "ERROR: /root/javatests/package_path/BUILD:42:12: "
                 + "Target '//java/package_path:helloroot_visibility' failed");
     assertNotNull(issue);
-    assertThat(issue.getFile().getPath()).isEqualTo("/path/to/root/javatests/package_path/BUILD");
+    assertThat(issue.getFile().getPath()).isEqualTo("/root/javatests/package_path/BUILD");
     assertThat(issue.getLine()).isEqualTo(42);
+    assertThat(issue.getColumn()).isEqualTo(12);
     assertThat(issue.getMessage())
         .isEqualTo("Target '//java/package_path:helloroot_visibility' failed");
     assertThat(issue.getCategory()).isEqualTo(IssueOutput.Category.ERROR);
@@ -141,7 +151,7 @@
 
   @Test
   public void testParseLinelessBuildError() {
-    BlazeIssueParser blazeIssueParser = new BlazeIssueParser(project, workspaceRoot);
+    BlazeIssueParser blazeIssueParser = new BlazeIssueParser(parsers);
     IssueOutput issue =
         blazeIssueParser.parseIssue(
             "ERROR: /path/to/root/java/package_path/BUILD:char offsets 1222--1229: "
@@ -153,20 +163,37 @@
   }
 
   @Test
-  public void testLabelProjectViewParser() {
-    ProjectViewSet projectViewSet =
-        ProjectViewSet.builder()
-            .add(
-                new File(".blazeproject"),
-                ProjectView.builder()
-                    .add(
-                        ListSection.builder(TargetSection.KEY)
-                            .add(TargetExpression.fromString("//package/path:hello4")))
-                    .build())
-            .build();
-    when(projectViewManager.getProjectViewSet()).thenReturn(projectViewSet);
+  public void testParseFileNotFoundError() {
+    BlazeIssueParser blazeIssueParser = new BlazeIssueParser(parsers);
+    IssueOutput issue =
+        blazeIssueParser.parseIssue(
+            "ERROR: Extension file not found. Unable to load file '//third_party/bazel:tools/ide/"
+                + "intellij_info.bzl': file doesn't exist or isn't a file");
+    assertNotNull(issue);
+    assertThat(issue.getFile().getPath())
+        .isEqualTo("/root/third_party/bazel/tools/ide/intellij_info.bzl");
+    assertThat(issue.getMessage()).isEqualTo("file doesn't exist or isn't a file");
+    assertThat(issue.getCategory()).isEqualTo(IssueOutput.Category.ERROR);
+  }
 
-    BlazeIssueParser blazeIssueParser = new BlazeIssueParser(project, workspaceRoot);
+  @Test
+  public void testParseFileNotFoundErrorWithPackage() {
+    BlazeIssueParser blazeIssueParser = new BlazeIssueParser(parsers);
+    IssueOutput issue =
+        blazeIssueParser.parseIssue(
+            "ERROR: error loading package 'path/to/package': Extension file not found. Unable to"
+                + " load file '//third_party/bazel:tools/ide/intellij_info.bzl': file doesn't exist"
+                + " or isn't a file");
+    assertNotNull(issue);
+    assertThat(issue.getFile().getPath())
+        .isEqualTo("/root/third_party/bazel/tools/ide/intellij_info.bzl");
+    assertThat(issue.getMessage()).isEqualTo("file doesn't exist or isn't a file");
+    assertThat(issue.getCategory()).isEqualTo(IssueOutput.Category.ERROR);
+  }
+
+  @Test
+  public void testLabelProjectViewParser() {
+    BlazeIssueParser blazeIssueParser = new BlazeIssueParser(parsers);
     IssueOutput issue =
         blazeIssueParser.parseIssue(
             "no such target '//package/path:hello4': "
@@ -179,19 +206,7 @@
 
   @Test
   public void testPackageProjectViewParser() {
-    ProjectViewSet projectViewSet =
-        ProjectViewSet.builder()
-            .add(
-                new File(".blazeproject"),
-                ProjectView.builder()
-                    .add(
-                        ListSection.builder(TargetSection.KEY)
-                            .add(TargetExpression.fromString("//package/path:hello4")))
-                    .build())
-            .build();
-    when(projectViewManager.getProjectViewSet()).thenReturn(projectViewSet);
-
-    BlazeIssueParser blazeIssueParser = new BlazeIssueParser(project, workspaceRoot);
+    BlazeIssueParser blazeIssueParser = new BlazeIssueParser(parsers);
     IssueOutput issue =
         blazeIssueParser.parseIssue(
             "no such package 'package/path': BUILD file not found on package path");
@@ -202,19 +217,7 @@
 
   @Test
   public void testDeletedBUILDFileButLeftPackageInLocalTargets() {
-    ProjectViewSet projectViewSet =
-        ProjectViewSet.builder()
-            .add(
-                new File(".blazeproject"),
-                ProjectView.builder()
-                    .add(
-                        ListSection.builder(TargetSection.KEY)
-                            .add(TargetExpression.fromString("//tests/com/google/a/b/c/d/baz:baz")))
-                    .build())
-            .build();
-    when(projectViewManager.getProjectViewSet()).thenReturn(projectViewSet);
-
-    BlazeIssueParser blazeIssueParser = new BlazeIssueParser(project, workspaceRoot);
+    BlazeIssueParser blazeIssueParser = new BlazeIssueParser(parsers);
     IssueOutput issue =
         blazeIssueParser.parseIssue(
             "Error:com.google.a.b.Exception exception in Bar: no targets found beneath "
@@ -240,7 +243,7 @@
           "name 'BAD_FUNCTION' is not defined."
         };
 
-    BlazeIssueParser blazeIssueParser = new BlazeIssueParser(project, workspaceRoot);
+    BlazeIssueParser blazeIssueParser = new BlazeIssueParser(parsers);
     for (int i = 0; i < lines.length - 1; ++i) {
       IssueOutput issue = blazeIssueParser.parseIssue(lines[i]);
       assertNull(issue);
@@ -266,7 +269,7 @@
           "name 'BAD_FUNCTION' is not defined."
         };
 
-    BlazeIssueParser blazeIssueParser = new BlazeIssueParser(project, workspaceRoot);
+    BlazeIssueParser blazeIssueParser = new BlazeIssueParser(parsers);
     for (int i = 0; i < lines.length; ++i) {
       blazeIssueParser.parseIssue(lines[i]);
     }
@@ -282,7 +285,7 @@
 
   @Test
   public void testMultipleIssues() {
-    BlazeIssueParser blazeIssueParser = new BlazeIssueParser(project, workspaceRoot);
+    BlazeIssueParser blazeIssueParser = new BlazeIssueParser(parsers);
     IssueOutput issue =
         blazeIssueParser.parseIssue(
             "ERROR: /home/plumpy/whatever:char offsets 1222--1229: name 'grubber' is not defined");
@@ -295,5 +298,32 @@
         blazeIssueParser.parseIssue(
             "ERROR: /home/plumpy/whatever:char offsets 1222--1229: name 'grubber' is not defined");
     assertNotNull(issue);
+
+  }
+
+  @Test
+  public void testExtraParserMatch() {
+    BlazeIssueParser blazeIssueParser = new BlazeIssueParser(ImmutableList.of(new TestParser()));
+    IssueOutput issue =
+        blazeIssueParser.parseIssue("TEST This is a test message for our test parser.");
+    assertNotNull(issue);
+    assertThat(issue.getMessage()).isEqualTo("This is a test message for our test parser.");
+    assertThat(issue.getLine()).isEqualTo(-1);
+    assertThat(issue.getColumn()).isEqualTo(-1);
+    assertThat(issue.getCategory()).isEqualTo(Category.WARNING);
+    assertNull(issue.getFile());
+  }
+
+  /** Simple Parser for testing */
+  private static class TestParser extends BlazeIssueParser.SingleLineParser {
+
+    public TestParser() {
+      super("^TEST (.*)$");
+    }
+
+    @Override
+    protected IssueOutput createIssue(Matcher matcher) {
+      return IssueOutput.warn(matcher.group(1)).build();
+    }
   }
 }
diff --git a/base/tests/unittests/com/google/idea/blaze/base/sync/projectview/SourceTestConfigTest.java b/base/tests/unittests/com/google/idea/blaze/base/sync/projectview/SourceTestConfigTest.java
new file mode 100644
index 0000000..5f2051f
--- /dev/null
+++ b/base/tests/unittests/com/google/idea/blaze/base/sync/projectview/SourceTestConfigTest.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.sync.projectview;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** Unit tests for {@link SourceTestConfig} */
+@RunWith(JUnit4.class)
+public class SourceTestConfigTest {
+
+  @Test
+  public void testGlobModification() {
+    assertThat(SourceTestConfig.modifyPattern("path/to/file/*")).isEqualTo("path/to/file*");
+    assertThat(SourceTestConfig.modifyPattern("path/to/file/")).isEqualTo("path/to/file*");
+    assertThat(SourceTestConfig.modifyPattern("path/to/file")).isEqualTo("path/to/file*");
+    assertThat(SourceTestConfig.modifyPattern("path/to/file*")).isEqualTo("path/to/file*");
+  }
+}
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 1424a0d..b197413 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
@@ -44,6 +44,7 @@
 import com.google.idea.blaze.base.settings.BlazeImportSettings;
 import com.google.idea.blaze.base.settings.BlazeImportSettingsManager;
 import com.google.idea.blaze.base.sync.aspects.BlazeIdeInterface;
+import com.google.idea.blaze.base.sync.data.BlazeDataStorage;
 import com.google.idea.blaze.base.sync.projectstructure.ModuleEditorImpl;
 import com.google.idea.blaze.base.sync.projectstructure.ModuleEditorProvider;
 import com.google.idea.blaze.base.sync.projectview.WorkspaceLanguageSettings;
@@ -53,8 +54,10 @@
 import com.google.idea.blaze.base.sync.workspace.WorkspacePathResolverImpl;
 import com.google.idea.blaze.base.vcs.BlazeVcsHandler;
 import com.intellij.openapi.project.Project;
+import com.intellij.openapi.roots.ContentEntry;
 import com.intellij.openapi.roots.ModifiableRootModel;
 import com.intellij.openapi.util.Disposer;
+import com.intellij.openapi.vfs.VirtualFile;
 import java.util.List;
 import java.util.Map;
 import javax.annotation.Nullable;
@@ -79,6 +82,8 @@
   protected ErrorCollector errorCollector;
   protected BlazeContext context;
 
+  private ImmutableList<ContentEntry> workspaceContentEntries = ImmutableList.of();
+
   @Before
   public void doSetup() throws Exception {
     projectViewManager = new MockProjectViewManager();
@@ -99,9 +104,12 @@
               @Override
               public void commit() {
                 // don't commit module changes,
-                // but make sure they're properly disposed when the test is finished
+                // and make sure they're properly disposed when the test is finished
                 for (ModifiableRootModel model : modifiableModels) {
                   Disposer.register(getTestRootDisposable(), model::dispose);
+                  if (model.getModule().getName().equals(BlazeDataStorage.WORKSPACE_MODULE_NAME)) {
+                    workspaceContentEntries = ImmutableList.copyOf(model.getContentEntries());
+                  }
                 }
               }
             };
@@ -128,6 +136,22 @@
             workspaceRoot.toString()));
   }
 
+  /** The workspace content entries created during sync */
+  protected ImmutableList<ContentEntry> getWorkspaceContentEntries() {
+    return workspaceContentEntries;
+  }
+
+  /** Search the workspace module's {@link ContentEntry}s for one with the given file. */
+  @Nullable
+  protected ContentEntry findContentEntry(VirtualFile root) {
+    for (ContentEntry entry : workspaceContentEntries) {
+      if (root.equals(entry.getFile())) {
+        return entry;
+      }
+    }
+    return null;
+  }
+
   protected static ArtifactLocation sourceRoot(String relativePath) {
     return ArtifactLocation.builder().setRelativePath(relativePath).setIsSource(true).build();
   }
diff --git a/clwb/src/com/google/idea/blaze/clwb/sync/BlazeCLionSyncPlugin.java b/clwb/src/com/google/idea/blaze/clwb/sync/BlazeCLionSyncPlugin.java
index 00888bc..64b1059 100644
--- a/clwb/src/com/google/idea/blaze/clwb/sync/BlazeCLionSyncPlugin.java
+++ b/clwb/src/com/google/idea/blaze/clwb/sync/BlazeCLionSyncPlugin.java
@@ -17,17 +17,12 @@
 
 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.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.google.idea.blaze.base.sync.GenericSourceFolderProvider;
+import com.google.idea.blaze.base.sync.SourceFolderProvider;
 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 {
@@ -52,20 +47,13 @@
     return null;
   }
 
+  @Nullable
   @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);
-      }
+  public SourceFolderProvider getSourceFolderProvider(BlazeProjectData projectData) {
+    if (!projectData.workspaceLanguageSettings.isWorkspaceType(WorkspaceType.C)) {
+      return null;
     }
+    return GenericSourceFolderProvider.INSTANCE;
   }
+
 }
diff --git a/common/formatter/BUILD b/common/formatter/BUILD
new file mode 100644
index 0000000..839a469
--- /dev/null
+++ b/common/formatter/BUILD
@@ -0,0 +1,11 @@
+licenses(["notice"])  # Apache 2.0
+
+java_library(
+    name = "formatter",
+    srcs = glob(["src/**/*.java"]),
+    visibility = ["//visibility:public"],
+    deps = [
+        "//intellij_platform_sdk:plugin_api",
+        "@jsr305_annotations//jar",
+    ],
+)
diff --git a/common/formatter/src/com/google/idea/common/formatter/DelegatingCodeStyleManager.java b/common/formatter/src/com/google/idea/common/formatter/DelegatingCodeStyleManager.java
new file mode 100644
index 0000000..aa2460f
--- /dev/null
+++ b/common/formatter/src/com/google/idea/common/formatter/DelegatingCodeStyleManager.java
@@ -0,0 +1,163 @@
+/*
+ * Copyright 2016 The Bazel Authors. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.idea.common.formatter;
+
+import com.intellij.lang.ASTNode;
+import com.intellij.openapi.editor.Document;
+import com.intellij.openapi.fileTypes.FileType;
+import com.intellij.openapi.project.Project;
+import com.intellij.openapi.util.Computable;
+import com.intellij.openapi.util.TextRange;
+import com.intellij.psi.PsiElement;
+import com.intellij.psi.PsiFile;
+import com.intellij.psi.codeStyle.CodeStyleManager;
+import com.intellij.psi.codeStyle.Indent;
+import com.intellij.util.IncorrectOperationException;
+import com.intellij.util.ThrowableRunnable;
+import java.util.Collection;
+import javax.annotation.Nullable;
+
+/** A delegating {@link CodeStyleManager}. */
+public abstract class DelegatingCodeStyleManager extends CodeStyleManager {
+
+  private final CodeStyleManager delegate;
+
+  public DelegatingCodeStyleManager(CodeStyleManager delegate) {
+    this.delegate = delegate;
+  }
+
+  @Override
+  public Project getProject() {
+    return delegate.getProject();
+  }
+
+  @Override
+  public PsiElement reformat(PsiElement element) throws IncorrectOperationException {
+    return delegate.reformat(element);
+  }
+
+  @Override
+  public PsiElement reformat(PsiElement element, boolean canChangeWhiteSpacesOnly)
+      throws IncorrectOperationException {
+    return delegate.reformat(element, canChangeWhiteSpacesOnly);
+  }
+
+  @Override
+  public PsiElement reformatRange(PsiElement element, int startOffset, int endOffset)
+      throws IncorrectOperationException {
+    return delegate.reformatRange(element, startOffset, endOffset);
+  }
+
+  @Override
+  public PsiElement reformatRange(
+      PsiElement element, int startOffset, int endOffset, boolean canChangeWhiteSpacesOnly)
+      throws IncorrectOperationException {
+    return delegate.reformatRange(element, startOffset, endOffset, canChangeWhiteSpacesOnly);
+  }
+
+  @Override
+  public void reformatText(PsiFile file, int startOffset, int endOffset)
+      throws IncorrectOperationException {
+    delegate.reformatText(file, startOffset, endOffset);
+  }
+
+  @Override
+  public void reformatText(PsiFile file, Collection<TextRange> ranges)
+      throws IncorrectOperationException {
+    delegate.reformatText(file, ranges);
+  }
+
+  @Override
+  public void reformatTextWithContext(PsiFile file, Collection<TextRange> ranges)
+      throws IncorrectOperationException {
+    delegate.reformatTextWithContext(file, ranges);
+  }
+
+  @Override
+  public void adjustLineIndent(PsiFile file, TextRange rangeToAdjust)
+      throws IncorrectOperationException {
+    delegate.adjustLineIndent(file, rangeToAdjust);
+  }
+
+  @Override
+  public int adjustLineIndent(PsiFile file, int offset) throws IncorrectOperationException {
+    return delegate.adjustLineIndent(file, offset);
+  }
+
+  @Override
+  public int adjustLineIndent(Document document, int offset) {
+    return delegate.adjustLineIndent(document, offset);
+  }
+
+  @Override
+  public boolean isLineToBeIndented(PsiFile file, int offset) {
+    return delegate.isLineToBeIndented(file, offset);
+  }
+
+  @Override
+  @Nullable
+  public String getLineIndent(PsiFile file, int offset) {
+    return delegate.getLineIndent(file, offset);
+  }
+
+  @Override
+  @Nullable
+  public String getLineIndent(Document document, int offset) {
+    return delegate.getLineIndent(document, offset);
+  }
+
+  @Override
+  public Indent getIndent(String text, FileType fileType) {
+    return delegate.getIndent(text, fileType);
+  }
+
+  @Override
+  public String fillIndent(Indent indent, FileType fileType) {
+    return delegate.fillIndent(indent, fileType);
+  }
+
+  @Override
+  public Indent zeroIndent() {
+    return delegate.zeroIndent();
+  }
+
+  @Override
+  public void reformatNewlyAddedElement(ASTNode block, ASTNode addedElement)
+      throws IncorrectOperationException {
+    delegate.reformatNewlyAddedElement(block, addedElement);
+  }
+
+  @Override
+  public boolean isSequentialProcessingAllowed() {
+    return delegate.isSequentialProcessingAllowed();
+  }
+
+  @Override
+  public void performActionWithFormatterDisabled(Runnable r) {
+    delegate.performActionWithFormatterDisabled(r);
+  }
+
+  @Override
+  public <T extends Throwable> void performActionWithFormatterDisabled(ThrowableRunnable<T> r)
+      throws T {
+    delegate.performActionWithFormatterDisabled(r);
+  }
+
+  @Override
+  public <T> T performActionWithFormatterDisabled(Computable<T> r) {
+    return delegate.performActionWithFormatterDisabled(r);
+  }
+}
diff --git a/common/formatter/src/com/google/idea/common/formatter/FormatterInstaller.java b/common/formatter/src/com/google/idea/common/formatter/FormatterInstaller.java
new file mode 100644
index 0000000..390dfb4
--- /dev/null
+++ b/common/formatter/src/com/google/idea/common/formatter/FormatterInstaller.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.common.formatter;
+
+import com.intellij.openapi.project.Project;
+import com.intellij.psi.codeStyle.CodeStyleManager;
+import org.picocontainer.MutablePicoContainer;
+
+/** A utility class to replace the default IntelliJ {@link CodeStyleManager}. */
+public final class FormatterInstaller {
+
+  /** A factory for constructing a {@link CodeStyleManager} given the current instance. */
+  public interface CodeStyleManagerFactory {
+    CodeStyleManager createFormatter(CodeStyleManager delegate);
+  }
+
+  private static final String CODE_STYLE_MANAGER_KEY = CodeStyleManager.class.getName();
+
+  /**
+   * Replace the existing formatter with one produced from the given {@link CodeStyleManagerFactory}
+   */
+  public static void replaceFormatter(Project project, CodeStyleManagerFactory newFormatter) {
+    CodeStyleManager currentManager = CodeStyleManager.getInstance(project);
+    MutablePicoContainer container = (MutablePicoContainer) project.getPicoContainer();
+    container.unregisterComponent(CODE_STYLE_MANAGER_KEY);
+    container.registerComponentInstance(
+        CODE_STYLE_MANAGER_KEY, newFormatter.createFormatter(currentManager));
+  }
+}
diff --git a/cpp/BUILD b/cpp/BUILD
index f38d357..d5361bf 100644
--- a/cpp/BUILD
+++ b/cpp/BUILD
@@ -1,5 +1,7 @@
 licenses(["notice"])  # Apache 2.0
 
+load("//intellij_platform_sdk:build_defs.bzl", "select_for_plugin_api")
+
 java_library(
     name = "cpp",
     srcs = glob(
@@ -7,13 +9,14 @@
         exclude = [
             "src/com/google/idea/blaze/cpp/versioned/**",
         ],
-    ) + select({
-        "//intellij_platform_sdk:android-studio-latest": [":api_v145_sources"],
-        "//conditions:default": [":api_v162_sources"],
+    ) + select_for_plugin_api({
+        "android-studio-145.1617.8": [":api_v145_sources"],
+        "default": [":api_v162_sources"],
     }),
     visibility = ["//visibility:public"],
     deps = [
         "//base",
+        "//common/experiments",
         "//intellij_platform_sdk:plugin_api",
         "@jsr305_annotations//jar",
     ],
diff --git a/cpp/src/com/google/idea/blaze/cpp/BlazeCWorkspace.java b/cpp/src/com/google/idea/blaze/cpp/BlazeCWorkspace.java
index 2795722..1b4937b 100644
--- a/cpp/src/com/google/idea/blaze/cpp/BlazeCWorkspace.java
+++ b/cpp/src/com/google/idea/blaze/cpp/BlazeCWorkspace.java
@@ -20,12 +20,15 @@
 import com.google.idea.blaze.base.model.BlazeProjectData;
 import com.google.idea.blaze.base.scope.BlazeContext;
 import com.google.idea.blaze.base.settings.Blaze;
+import com.google.idea.common.experiments.BoolExperiment;
 import com.intellij.openapi.application.ApplicationManager;
 import com.intellij.openapi.components.ServiceManager;
 import com.intellij.openapi.diagnostic.Logger;
 import com.intellij.openapi.project.Project;
 import com.intellij.openapi.vfs.LocalFileSystem;
+import com.intellij.openapi.vfs.VfsUtil;
 import com.intellij.openapi.vfs.VirtualFile;
+import com.intellij.openapi.vfs.ex.temp.TempFileSystem;
 import com.jetbrains.cidr.lang.symbols.OCSymbol;
 import com.jetbrains.cidr.lang.workspace.OCResolveConfiguration;
 import com.jetbrains.cidr.lang.workspace.OCWorkspace;
@@ -38,6 +41,9 @@
 public final class BlazeCWorkspace implements OCWorkspace {
   private static final Logger LOG = Logger.getInstance(BlazeCWorkspace.class);
 
+  private static final BoolExperiment refreshExecRoot =
+      new BoolExperiment("refresh.exec.root.cpp", true);
+
   @Nullable private final Project project;
   @Nullable private final OCWorkspaceModificationTrackers modTrackers;
 
@@ -65,13 +71,9 @@
 
     long start = System.currentTimeMillis();
 
-    // non-recursive refresh of the blaze-out directory. This essentially invalidates the cache for
-    // all files below this directory.
-    ApplicationManager.getApplication()
-        .runWriteAction(
-            () ->
-                LocalFileSystem.getInstance()
-                    .refreshIoFiles(ImmutableList.of(blazeProjectData.blazeRoots.executionRoot)));
+    if (refreshExecRoot.getValue()) {
+      refreshExecRoot(blazeProjectData);
+    }
 
     // Non-incremental update to our c configurations.
     configurationResolver.update(context, blazeProjectData);
@@ -93,6 +95,19 @@
             });
   }
 
+  private static void refreshExecRoot(BlazeProjectData blazeProjectData) {
+    // recursive refresh of the blaze execution root. This is required because:
+    // <li>Our blaze aspect can't tell us exactly which genfiles are required to resolve the project
+    // <li>Cidr caches the directory contents as part of symbol building, so we need to do this work
+    // up front.
+    VirtualFile execRoot =
+        getFileSystem().findFileByIoFile(blazeProjectData.blazeRoots.executionRoot);
+    if (execRoot != null) {
+      ApplicationManager.getApplication()
+          .runWriteAction(() -> VfsUtil.markDirtyAndRefresh(false, true, true, execRoot));
+    }
+  }
+
   @Override
   public Collection<VirtualFile> getLibraryFilesToBuildSymbols() {
     // This method should return all the header files themselves, not the head file directories.
@@ -148,4 +163,11 @@
     OCResolveConfiguration config = configurationResolver.getConfigurationForFile(sourceFile);
     return config == null ? ImmutableList.of() : ImmutableList.of(config);
   }
+
+  private static LocalFileSystem getFileSystem() {
+    if (ApplicationManager.getApplication().isUnitTestMode()) {
+      return TempFileSystem.getInstance();
+    }
+    return LocalFileSystem.getInstance();
+  }
 }
diff --git a/ijwb/BUILD b/ijwb/BUILD
index d327898..0ec5aad 100644
--- a/ijwb/BUILD
+++ b/ijwb/BUILD
@@ -66,6 +66,7 @@
 
 load(
     "//testing:test_defs.bzl",
+    "intellij_integration_test_suite",
     "intellij_unit_test_suite",
 )
 
@@ -84,3 +85,22 @@
         "@junit//jar",
     ],
 )
+
+intellij_integration_test_suite(
+    name = "integration_tests",
+    srcs = glob(["tests/integrationtests/**/*.java"]),
+    required_plugins = "com.google.idea.blaze.ijwb",
+    test_package_root = "com.google.idea.blaze.ijwb",
+    runtime_deps = [
+        ":ijwb_bazel",
+    ],
+    deps = [
+        ":ijwb_lib",
+        "//base",
+        "//base:integration_test_utils",
+        "//base:unit_test_utils",
+        "//intellij_platform_sdk:plugin_api_for_tests",
+        "@jsr305_annotations//jar",
+        "@junit//jar",
+    ],
+)
diff --git a/ijwb/src/META-INF/ijwb_bazel.xml b/ijwb/src/META-INF/ijwb_bazel.xml
index 00430e9..3d7fbc2 100644
--- a/ijwb/src/META-INF/ijwb_bazel.xml
+++ b/ijwb/src/META-INF/ijwb_bazel.xml
@@ -30,10 +30,4 @@
       ]]>
   </description>
 
-  <application-components>
-    <component>
-      <implementation-class>com.google.idea.blaze.ijwb.plugin.MigrateBazelPluginDependency</implementation-class>
-    </component>
-  </application-components>
-
 </idea-plugin>
diff --git a/ijwb/src/com/google/idea/blaze/ijwb/android/BlazeAndroidLiteJavaSyncAugmenter.java b/ijwb/src/com/google/idea/blaze/ijwb/android/BlazeAndroidLiteJavaSyncAugmenter.java
index 6a2b844..69ec1d8 100644
--- a/ijwb/src/com/google/idea/blaze/ijwb/android/BlazeAndroidLiteJavaSyncAugmenter.java
+++ b/ijwb/src/com/google/idea/blaze/ijwb/android/BlazeAndroidLiteJavaSyncAugmenter.java
@@ -19,6 +19,7 @@
 import com.google.idea.blaze.base.ideinfo.LibraryArtifact;
 import com.google.idea.blaze.base.ideinfo.TargetIdeInfo;
 import com.google.idea.blaze.base.model.primitives.LanguageClass;
+import com.google.idea.blaze.base.projectview.ProjectViewSet;
 import com.google.idea.blaze.base.sync.projectview.WorkspaceLanguageSettings;
 import com.google.idea.blaze.java.sync.BlazeJavaSyncAugmenter;
 import com.google.idea.blaze.java.sync.model.BlazeJarLibrary;
@@ -30,6 +31,7 @@
   @Override
   public void addJarsForSourceTarget(
       WorkspaceLanguageSettings workspaceLanguageSettings,
+      ProjectViewSet projectViewSet,
       TargetIdeInfo target,
       Collection<BlazeJarLibrary> jars,
       Collection<BlazeJarLibrary> genJars) {
diff --git a/ijwb/src/com/google/idea/blaze/ijwb/javascript/BlazeJavascriptSyncPlugin.java b/ijwb/src/com/google/idea/blaze/ijwb/javascript/BlazeJavascriptSyncPlugin.java
index 4d19ffc..c684726 100644
--- a/ijwb/src/com/google/idea/blaze/ijwb/javascript/BlazeJavascriptSyncPlugin.java
+++ b/ijwb/src/com/google/idea/blaze/ijwb/javascript/BlazeJavascriptSyncPlugin.java
@@ -20,27 +20,25 @@
 import com.google.common.collect.Lists;
 import com.google.idea.blaze.base.model.BlazeProjectData;
 import com.google.idea.blaze.base.model.primitives.LanguageClass;
-import com.google.idea.blaze.base.model.primitives.WorkspacePath;
 import com.google.idea.blaze.base.model.primitives.WorkspaceRoot;
 import com.google.idea.blaze.base.model.primitives.WorkspaceType;
 import com.google.idea.blaze.base.projectview.ProjectViewSet;
 import com.google.idea.blaze.base.scope.BlazeContext;
 import com.google.idea.blaze.base.scope.output.IssueOutput;
 import com.google.idea.blaze.base.sync.BlazeSyncPlugin;
+import com.google.idea.blaze.base.sync.GenericSourceFolderProvider;
+import com.google.idea.blaze.base.sync.SourceFolderProvider;
 import com.google.idea.blaze.base.sync.libraries.LibrarySource;
-import com.google.idea.blaze.base.sync.projectview.SourceTestConfig;
 import com.google.idea.blaze.base.sync.projectview.WorkspaceLanguageSettings;
+import com.intellij.openapi.application.ApplicationManager;
 import com.intellij.openapi.module.Module;
 import com.intellij.openapi.module.ModuleType;
 import com.intellij.openapi.module.WebModuleType;
 import com.intellij.openapi.project.Project;
-import com.intellij.openapi.roots.ContentEntry;
 import com.intellij.openapi.roots.ModifiableRootModel;
 import com.intellij.openapi.roots.libraries.Library;
 import com.intellij.openapi.roots.libraries.LibraryTablesRegistrar;
-import com.intellij.openapi.vfs.VirtualFile;
 import com.intellij.util.PlatformUtils;
-import java.util.Collection;
 import java.util.List;
 import java.util.Set;
 import javax.annotation.Nullable;
@@ -67,31 +65,13 @@
     return ImmutableSet.of(LanguageClass.JAVASCRIPT);
   }
 
+  @Nullable
   @Override
-  public void updateContentEntries(
-      Project project,
-      BlazeContext context,
-      WorkspaceRoot workspaceRoot,
-      ProjectViewSet projectViewSet,
-      BlazeProjectData blazeProjectData,
-      Collection<ContentEntry> contentEntries) {
-    if (!blazeProjectData.workspaceLanguageSettings.isWorkspaceType(WorkspaceType.JAVASCRIPT)) {
-      return;
+  public SourceFolderProvider getSourceFolderProvider(BlazeProjectData projectData) {
+    if (!projectData.workspaceLanguageSettings.isWorkspaceType(WorkspaceType.JAVASCRIPT)) {
+      return null;
     }
-
-    SourceTestConfig testConfig = new SourceTestConfig(projectViewSet);
-    for (ContentEntry contentEntry : contentEntries) {
-      VirtualFile virtualFile = contentEntry.getFile();
-      if (virtualFile == null) {
-        continue;
-      }
-      if (!workspaceRoot.isInWorkspace(virtualFile)) {
-        continue;
-      }
-      WorkspacePath workspacePath = workspaceRoot.workspacePathFor(virtualFile);
-      boolean isTestSource = testConfig.isTestSource(workspacePath.relativePath());
-      contentEntry.addSourceFolder(virtualFile, isTestSource);
-    }
+    return GenericSourceFolderProvider.INSTANCE;
   }
 
   @Override
@@ -149,7 +129,7 @@
     if (!workspaceLanguageSettings.isLanguageActive(LanguageClass.JAVASCRIPT)) {
       return true;
     }
-    if (!PlatformUtils.isIdeaUltimate()) {
+    if (!ApplicationManager.getApplication().isUnitTestMode() && !PlatformUtils.isIdeaUltimate()) {
       IssueOutput.error("IntelliJ Ultimate needed for Javascript support.").submit(context);
       return false;
     }
diff --git a/ijwb/src/com/google/idea/blaze/ijwb/plugin/MigrateBazelPluginDependency.java b/ijwb/src/com/google/idea/blaze/ijwb/plugin/MigrateBazelPluginDependency.java
deleted file mode 100644
index 4231e03..0000000
--- a/ijwb/src/com/google/idea/blaze/ijwb/plugin/MigrateBazelPluginDependency.java
+++ /dev/null
@@ -1,48 +0,0 @@
-/*
- * Copyright 2016 The Bazel Authors. All rights reserved.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *    http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.google.idea.blaze.ijwb.plugin;
-
-import com.google.idea.blaze.base.plugin.dependency.PluginDependencyHelper;
-import com.google.idea.blaze.base.settings.Blaze;
-import com.google.idea.blaze.base.settings.Blaze.BuildSystem;
-import com.intellij.openapi.components.ApplicationComponent;
-import com.intellij.openapi.project.Project;
-import com.intellij.openapi.project.ProjectManager;
-import com.intellij.openapi.project.ProjectManagerAdapter;
-
-/**
- * Temporary migration code following the IntelliJ-with-Bazel plugin ID change. We can't prevent an
- * initial, spurious error that a required plugin is missing, however this will at least prevent the
- * error on subsequent project loads.
- */
-public class MigrateBazelPluginDependency extends ApplicationComponent.Adapter {
-
-  @Override
-  public void initComponent() {
-    ProjectManager projectManager = ProjectManager.getInstance();
-    projectManager.addProjectManagerListener(
-        new ProjectManagerAdapter() {
-          @Override
-          public void projectOpened(Project project) {
-            if (Blaze.isBlazeProject(project)
-                && Blaze.getBuildSystem(project) == BuildSystem.Bazel) {
-              PluginDependencyHelper.removeDependencyOnOldPlugin(
-                  project, IjwbPluginId.BLAZE_PLUGIN_ID);
-            }
-          }
-        });
-  }
-}
diff --git a/ijwb/tests/integrationtests/com/google/idea/blaze/ijwb/javascript/JavascriptSyncTest.java b/ijwb/tests/integrationtests/com/google/idea/blaze/ijwb/javascript/JavascriptSyncTest.java
new file mode 100644
index 0000000..5174f96
--- /dev/null
+++ b/ijwb/tests/integrationtests/com/google/idea/blaze/ijwb/javascript/JavascriptSyncTest.java
@@ -0,0 +1,156 @@
+/*
+ * Copyright 2016 The Bazel Authors. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.idea.blaze.ijwb.javascript;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.google.common.collect.ImmutableList;
+import com.google.idea.blaze.base.model.primitives.WorkspacePath;
+import com.google.idea.blaze.base.sync.BlazeSyncIntegrationTestCase;
+import com.google.idea.blaze.base.sync.BlazeSyncParams;
+import com.intellij.openapi.roots.ContentEntry;
+import com.intellij.openapi.roots.SourceFolder;
+import com.intellij.openapi.vfs.VirtualFile;
+import javax.annotation.Nullable;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** Sync integration tests for projects containing javascript. */
+@RunWith(JUnit4.class)
+public class JavascriptSyncTest extends BlazeSyncIntegrationTestCase {
+
+  @Test
+  public void testSimpleTestSourcesIdentified() {
+    setProjectView(
+        "directories:",
+        "  common/jslayout/calendar",
+        "  common/jslayout/tests",
+        "targets:",
+        "  //common/jslayout/...:all",
+        "test_sources:",
+        "  common/jslayout/tests",
+        "workspace_type: javascript");
+
+    VirtualFile rootFile = workspace.createDirectory(new WorkspacePath("common/jslayout/calendar"));
+
+    VirtualFile testFile =
+        workspace.createFile(new WorkspacePath("common/jslayout/tests/date_formatter_test.js"));
+
+    BlazeSyncParams syncParams =
+        new BlazeSyncParams.Builder("Full Sync", BlazeSyncParams.SyncMode.FULL)
+            .addProjectViewTargets(true)
+            .build();
+    runBlazeSync(syncParams);
+
+    errorCollector.assertNoIssues();
+
+    assertThat(getWorkspaceContentEntries()).hasSize(2);
+
+    ContentEntry sourceEntry = findContentEntry(rootFile);
+    assertThat(sourceEntry.getSourceFolders()).hasLength(1);
+
+    SourceFolder nonTestSource = findSourceFolder(sourceEntry, rootFile);
+    assertThat(nonTestSource.isTestSource()).isFalse();
+
+    ContentEntry testEntry = findContentEntry(testFile.getParent());
+    assertThat(testEntry.getSourceFolders()).hasLength(1);
+
+    SourceFolder testSource = findSourceFolder(testEntry, testFile.getParent());
+    assertThat(testSource.isTestSource()).isTrue();
+  }
+
+  @Test
+  public void testTestSourcesMissingFromDirectoriesSectionAreAdded() {
+    setProjectView(
+        "directories:",
+        "  common/jslayout",
+        "targets:",
+        "  //common/jslayout/...:all",
+        "test_sources:",
+        "  */tests",
+        "workspace_type: javascript");
+
+    VirtualFile testDir = workspace.createDirectory(new WorkspacePath("common/jslayout/tests"));
+
+    BlazeSyncParams syncParams =
+        new BlazeSyncParams.Builder("Full Sync", BlazeSyncParams.SyncMode.FULL)
+            .addProjectViewTargets(true)
+            .build();
+    runBlazeSync(syncParams);
+
+    errorCollector.assertNoIssues();
+
+    ImmutableList<ContentEntry> contentEntries = getWorkspaceContentEntries();
+    assertThat(contentEntries).hasSize(1);
+
+    SourceFolder root = findSourceFolder(contentEntries.get(0), testDir.getParent());
+    assertThat(root.isTestSource()).isFalse();
+
+    SourceFolder testRoot = findSourceFolder(contentEntries.get(0), testDir);
+    assertThat(testRoot).isNotNull();
+    assertThat(testRoot.isTestSource()).isTrue();
+  }
+
+  @Test
+  public void testTestSourceChildrenAreNotAddedAsSourceFolders() {
+    // child directories of test sources are always test sources, so they should never
+    // appear as separate SourceFolders.
+    setProjectView(
+        "directories:",
+        "  common/jslayout",
+        "targets:",
+        "  //common/jslayout/...:all",
+        "test_sources:",
+        "  */tests/*",
+        "workspace_type: javascript");
+
+    VirtualFile rootDir = workspace.createDirectory(new WorkspacePath("common/jslayout"));
+    VirtualFile nestedTestDir =
+        workspace.createDirectory(new WorkspacePath("common/jslayout/tests/foo"));
+
+    BlazeSyncParams syncParams =
+        new BlazeSyncParams.Builder("Full Sync", BlazeSyncParams.SyncMode.FULL)
+            .addProjectViewTargets(true)
+            .build();
+    runBlazeSync(syncParams);
+
+    errorCollector.assertNoIssues();
+
+    ImmutableList<ContentEntry> contentEntries = getWorkspaceContentEntries();
+    assertThat(contentEntries).hasSize(1);
+
+    SourceFolder root = findSourceFolder(contentEntries.get(0), rootDir);
+    assertThat(root.isTestSource()).isFalse();
+
+    SourceFolder child = findSourceFolder(contentEntries.get(0), nestedTestDir);
+    assertThat(child).isNull();
+
+    SourceFolder testRoot = findSourceFolder(contentEntries.get(0), nestedTestDir.getParent());
+    assertThat(testRoot).isNotNull();
+    assertThat(testRoot.isTestSource()).isTrue();
+  }
+
+  @Nullable
+  private static SourceFolder findSourceFolder(ContentEntry entry, VirtualFile file) {
+    for (SourceFolder sourceFolder : entry.getSourceFolders()) {
+      if (file.equals(sourceFolder.getFile())) {
+        return sourceFolder;
+      }
+    }
+    return null;
+  }
+}
diff --git a/intellij_platform_sdk/BUILD b/intellij_platform_sdk/BUILD
index a0aad73..2f68436 100644
--- a/intellij_platform_sdk/BUILD
+++ b/intellij_platform_sdk/BUILD
@@ -1,6 +1,11 @@
+# Copyright 2011 Google Inc.  All rights reserved.
 #
-# Description: IntelliJ plugin SDKs required to build the plugin jars.
-#
+# Description:
+#   Defines a package group that restricts access to the JetBrains
+#   plugin apis to known packages that build plugins. Only packages
+#   listed here may depend on these libraries.
+
+licenses(["notice"])  # Apache2
 
 package(default_visibility = ["//visibility:public"])
 
@@ -11,10 +16,19 @@
     },
 )
 
+# IntelliJ CE 2016.3.1
 config_setting(
-    name = "clion-latest",
+    name = "intellij-2016.3.1",
     values = {
-        "define": "ij_product=clion-latest",
+        "define": "ij_product=intellij-2016.3.1",
+    },
+)
+
+# IntelliJ CE 2016.2.4
+config_setting(
+    name = "intellij-162.2032.8",
+    values = {
+        "define": "ij_product=intellij-162.2032.8",
     },
 )
 
@@ -25,18 +39,67 @@
     },
 )
 
+# Android Studio 2.2.0.7
+config_setting(
+    name = "android-studio-145.1617.8",
+    values = {
+        "define": "ij_product=android-studio-145.1617.8",
+    },
+)
+
+config_setting(
+    name = "android-studio-beta",
+    values = {
+        "define": "ij_product=android-studio-beta",
+    },
+)
+
+# Android Studio 2.3.0.3
+config_setting(
+    name = "android-studio-2.3.0.3",
+    values = {
+        "define": "ij_product=android-studio-2.3.0.3",
+    },
+)
+
+config_setting(
+    name = "clion-latest",
+    values = {
+        "define": "ij_product=clion-latest",
+    },
+)
+
+# CLion 2016.2.2
+config_setting(
+    name = "clion-162.1967.7",
+    values = {
+        "define": "ij_product=clion-162.1967.7",
+    },
+)
+
+# CLion 16 (2016.2.1)
+config_setting(
+    name = "clion-162.1628.20",
+    values = {
+        "define": "ij_product=clion-162.1628.20",
+    },
+)
+
+load(":build_defs.bzl", "select_from_plugin_api_directory", "select_for_ide")
+
+# The purpose of this rule is to hide the versioning
+# complexity from users of this api.
+# There will be additional versions added in the future
 java_library(
     name = "plugin_api_internal",
-    visibility = ["//visibility:private"],
-    exports = select({
-        ":intellij-latest": ["@intellij_latest//:plugin_api"],
-        ":clion-latest": ["@clion_latest//:plugin_api"],
-        ":android-studio-latest": [
-            "@android_studio_latest//:plugin_api",
-            "@android_studio_latest//:android_plugin",
+    exports = select_from_plugin_api_directory(
+        android_studio = [
+            ":sdk",
+            ":android_plugin",
         ],
-        "//conditions:default": ["@intellij_latest//:plugin_api"],
-    }),
+        clion = [":sdk"],
+        intellij = [":sdk"],
+    ),
 )
 
 # The outward facing plugin api
@@ -59,57 +122,73 @@
     ],
 )
 
+# For the grammar-kit binary only (a build time tool). It needs the IJ API for runtime.
+# The clion.jar doesn't work (missing MockEditorFactory, MockProjectEx), so just use
+# an idea.jar. It doesn't affect the generated code.
 java_library(
-    name = "junit",
-    neverlink = 1,
-    exports = select({
-        ":intellij-latest": ["@intellij_latest//:junit"],
-        ":android-studio-latest": ["@android_studio_latest//:junit"],
-        ":clion-latest": [],
-        "//conditions:default": ["@intellij_latest//:junit"],
-    }),
+    name = "plugin_api_for_grammar_kit",
+    visibility = ["//third_party/java/jetbrains/grammar_kit:__pkg__"],
+    exports = ["//intellij_platform_sdk/IC_162_2032_8:sdk"],
 )
 
-# The dev kit is only for IntelliJ since you only develop plugins in Java.
+# Used to support IntelliJ plugin development in our plugin
 java_library(
     name = "devkit",
     neverlink = 1,
-    exports = select({
-        ":intellij-latest": ["@intellij_latest//:devkit"],
-        ":android-studio-latest": [],
-        ":clion-latest": [],
-        "//conditions:default": ["@intellij_latest//:devkit"],
-    }),
+    exports = select_from_plugin_api_directory(
+        android_studio = [],
+        clion = [],
+        intellij = [":devkit"],
+    ),
+)
+
+# IntelliJ Mercurial plugin
+java_library(
+    name = "hg4idea",
+    neverlink = 1,
+    exports = select_from_plugin_api_directory(
+        android_studio = [":hg4idea"],
+        clion = [":hg4idea"],
+        intellij = [":hg4idea"],
+    ),
+)
+
+# IntelliJ JUnit plugin
+java_library(
+    name = "junit",
+    neverlink = 1,
+    exports = select_from_plugin_api_directory(
+        android_studio = [":junit"],
+        clion = [],
+        intellij = [":junit"],
+    ),
 )
 
 # Bundled plugins required by integration tests
 java_library(
     name = "bundled_plugins",
     testonly = 1,
-    runtime_deps = select({
-        ":intellij-latest": ["@intellij_latest//:bundled_plugins"],
-        ":clion-latest": ["@clion_latest//:bundled_plugins"],
-        ":android-studio-latest": ["@android_studio_latest//:bundled_plugins"],
-        "//conditions:default": ["@intellij_latest//:bundled_plugins"],
-    }),
+    runtime_deps = select_from_plugin_api_directory(
+        android_studio = [":bundled_plugins"],
+        clion = [":bundled_plugins"],
+        intellij = [":bundled_plugins"],
+    ),
 )
 
 filegroup(
     name = "application_info_jar",
-    srcs = select({
-        ":intellij-latest": ["@intellij_latest//:application_info_jar"],
-        ":clion-latest": ["@clion_latest//:application_info_jar"],
-        ":android-studio-latest": ["@android_studio_latest//:application_info_jar"],
-        "//conditions:default": ["@intellij_latest//:application_info_jar"],
-    }),
+    srcs = select_from_plugin_api_directory(
+        android_studio = [":application_info_jar"],
+        clion = [":application_info_jar"],
+        intellij = [":application_info_jar"],
+    ),
 )
 
 filegroup(
     name = "application_info_name",
-    srcs = select({
-        ":intellij-latest": ["intellij_application_info_name.txt"],
-        ":clion-latest": ["clion_application_info_name.txt"],
-        ":android-studio-latest": ["android_studio_application_info_name.txt"],
-        "//conditions:default": ["intellij_application_info_name.txt"],
-    }),
+    srcs = select_for_ide(
+        android_studio = ["android_studio_application_info_name.txt"],
+        clion = ["clion_application_info_name.txt"],
+        intellij = ["intellij_application_info_name.txt"],
+    ),
 )
diff --git a/intellij_platform_sdk/BUILD.android_studio b/intellij_platform_sdk/BUILD.android_studio
index 77c0f55..f768640 100644
--- a/intellij_platform_sdk/BUILD.android_studio
+++ b/intellij_platform_sdk/BUILD.android_studio
@@ -5,7 +5,7 @@
 package(default_visibility = ["//visibility:public"])
 
 java_import(
-    name = "plugin_api",
+    name = "sdk",
     jars = glob([
         "android-studio/lib/*.jar",
     ]),
diff --git a/intellij_platform_sdk/BUILD.clion b/intellij_platform_sdk/BUILD.clion
index c32194f..e395d09 100644
--- a/intellij_platform_sdk/BUILD.clion
+++ b/intellij_platform_sdk/BUILD.clion
@@ -5,11 +5,16 @@
 package(default_visibility = ["//visibility:public"])
 
 java_import(
-    name = "plugin_api",
+    name = "sdk",
     jars = glob(["clion-*/lib/*.jar"]),
     tags = ["intellij-provided-by-sdk"],
 )
 
+java_import(
+    name = "hg4idea",
+    jars = glob(["clion-*/plugins/hg4idea/lib/hg4idea.jar"]),
+)
+
 # The plugins required by CLwB. Presumably there will be some, when we write
 # some integration tests.
 java_import(
@@ -21,4 +26,4 @@
 filegroup(
     name = "application_info_jar",
     srcs = glob(["clion-*/lib/clion.jar"]),
-)
\ No newline at end of file
+)
diff --git a/intellij_platform_sdk/BUILD.idea b/intellij_platform_sdk/BUILD.idea
index 22082d2..32a92ad 100644
--- a/intellij_platform_sdk/BUILD.idea
+++ b/intellij_platform_sdk/BUILD.idea
@@ -5,19 +5,24 @@
 package(default_visibility = ["//visibility:public"])
 
 java_import(
-    name = "plugin_api",
-    jars = glob(["idea-IC-*/lib/*.jar"]),
+    name = "sdk",
+    jars = glob(["lib/*.jar"]),
     tags = ["intellij-provided-by-sdk"],
 )
 
 java_import(
     name = "devkit",
-    jars = glob(["idea-IC-*/plugins/devkit/lib/devkit.jar"]),
+    jars = glob(["plugins/devkit/lib/devkit.jar"]),
+)
+
+java_import(
+    name = "hg4idea",
+    jars = ["plugins/hg4idea/lib/hg4idea.jar"],
 )
 
 java_import(
     name = "junit",
-    jars = glob(["idea-IC-*/plugins/junit/lib/*.jar"]),
+    jars = glob(["plugins/junit/lib/*.jar"]),
 )
 
 # The plugins required by IJwB. We need to include them
@@ -25,15 +30,15 @@
 java_import(
     name = "bundled_plugins",
     jars = glob([
-        "idea-IC-*/plugins/devkit/lib/*.jar",
-        "idea-IC-*/plugins/java-i18n/lib/*.jar",
-        "idea-IC-*/plugins/junit/lib/*.jar",
-        "idea-IC-*/plugins/properties/lib/*.jar",
+        "plugins/devkit/lib/*.jar",
+        "plugins/java-i18n/lib/*.jar",
+        "plugins/junit/lib/*.jar",
+        "plugins/properties/lib/*.jar",
     ]),
     tags = ["intellij-provided-by-sdk"],
 )
 
 filegroup(
     name = "application_info_jar",
-    srcs = glob(["idea-IC-*/lib/resources.jar"]),
+    srcs = glob(["lib/resources.jar"]),
 )
diff --git a/intellij_platform_sdk/build_defs.bzl b/intellij_platform_sdk/build_defs.bzl
new file mode 100644
index 0000000..57dbe76
--- /dev/null
+++ b/intellij_platform_sdk/build_defs.bzl
@@ -0,0 +1,169 @@
+"""Convenience methods for plugin_api."""
+
+# The current indirect ij_product mapping (eg. "intellij-latest")
+INDIRECT_IJ_PRODUCTS = {
+    "intellij-latest": "intellij-162.2032.8",
+    "android-studio-latest": "android-studio-145.1617.8",
+    "android-studio-beta": "android-studio-2.3.0.3",
+    "clion-latest": "clion-162.1967.7",
+}
+
+DIRECT_IJ_PRODUCTS = {
+    "intellij-2016.3.1": struct(
+        ide="intellij",
+        directory="intellij_ce_2016_3_1",
+    ),
+    "intellij-162.2032.8": struct(
+        ide="intellij",
+        directory="IC_162_2032_8",
+    ),
+    "android-studio-145.1617.8": struct(
+        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",
+    ),
+    "clion-162.1628.20": struct(
+        ide="clion",
+        directory="CL_162_1628_20",
+    ),
+    "clion-162.1967.7": struct(
+        ide="clion",
+        directory="CL_162_1967_7",
+    ),
+}
+
+# BUILD_VARS for each IDE corresponding to indirect ij_products, eg. "intellij-latest"
+
+
+
+
+
+
+def select_for_plugin_api(params):
+  """Selects for a plugin_api.
+
+  Args:
+      params: A dict with ij_product -> value.
+              You may only include direct ij_products here,
+              not indirects (eg. intellij-latest).
+  Returns:
+      A select statement on all plugin_apis. Unless you include a "default"
+      clause any other matched plugin_api will return "None".
+
+      A build without an ij_product is considered equivalent to building with
+      "intellij-latest".
+
+  Example:
+    java_library(
+      name = "foo",
+      srcs = select_for_plugin_api({
+          "intellij-2016.3.1": [...my intellij 2016.3 sources ....],
+          "intellij-2012.2.4": [...my intellij 2016.2 sources ...],
+      }),
+    )
+  """
+  for indirect_ij_product in INDIRECT_IJ_PRODUCTS:
+    if indirect_ij_product in params:
+      error_message = "".join([
+          "Do not select on indirect ij_product %s. " % indirect_ij_product,
+          "Instead, select on an exact ij_product."])
+      fail(error_message)
+
+  # To make the select work with "intellij-latest" and friends,
+  # we find if the user is currently selecting on what intellij-latest
+  # is resolving to, and copy that. Example:
+  #
+  # {"intellij-2016.3.1": "stuff"} ->
+  # {"intellij-2016.3.1": "stuff", "intellij-latest": "stuff"}
+  params = dict(**params)
+  for indirect_ij_product, resolved_plugin_api in INDIRECT_IJ_PRODUCTS.items():
+    if resolved_plugin_api in params:
+      params[indirect_ij_product] = params[resolved_plugin_api]
+
+  if "default" not in params:
+    # If "intellij-latest" is supported, we set "default" to that
+    # This supports building with an empty command line. Example:
+    #
+    # {"intellij-2016.3.1": "stuff", "intellij-latest": "stuff"} ->
+    # {"intellij-2016.3.1": "stuff", "intellij-latest": "stuff", "default": "stuff"}
+    if "intellij-latest" in params:
+      params["default"] = params["intellij-latest"]
+
+    # Add the other indirect ij_products returning None rather than default
+    for ij_product in INDIRECT_IJ_PRODUCTS:
+      if ij_product not in params:
+        params[ij_product] = None
+    for ij_product in DIRECT_IJ_PRODUCTS:
+      if ij_product not in params:
+        params[ij_product] = None
+
+  # Map to the actual targets
+  # This makes it more convenient so the user doesn't have to
+  # fully specify the path to the plugin_apis
+  select_params = dict()
+  for ij_product, value in params.items():
+    if ij_product == "default":
+      select_params["//conditions:default"] = value
+    else:
+      select_params["//intellij_platform_sdk:" + ij_product] = value
+  return select(select_params)
+
+def select_for_ide(intellij=None, android_studio=None, clion=None, default=None):
+  """Selects for the supported IDEs.
+
+  Args:
+      intellij: Files to use for IntelliJ. If None, will use default.
+      android_studio: Files to use for Android Studio. If None will use default.
+      clion: Files to use for CLion. If None will use default.
+      default: Files to use for any IDEs not passed.
+  Returns:
+      A select statement on all plugin_apis, sorted into IDEs.
+
+  Example:
+    java_library(
+      name = "foo",
+      srcs = select_for_ide(
+          clion = [":cpp_only_sources"],
+          default = [":java_only_sources"],
+      ),
+    )
+  """
+  intellij = intellij or default
+  android_studio = android_studio or default
+  clion = clion or default
+  default = default or intellij
+
+  ide_to_value = {
+      "intellij" : intellij,
+      "android-studio": android_studio,
+      "clion": clion,
+  }
+
+  # Map (direct ij_product) -> corresponding ide value
+  params = dict()
+  for ij_product, value in DIRECT_IJ_PRODUCTS.items():
+    params[ij_product] = ide_to_value[value.ide]
+  params["default"] = default
+
+  return select_for_plugin_api(params)
+
+def _plugin_api_directory(value):
+  return "@" + value.directory + "//"
+
+def select_from_plugin_api_directory(intellij, android_studio, clion):
+  """Internal convenience method to generate select statement from the IDE's plugin_api directories."""
+
+  ide_to_value = {
+      "intellij" : intellij,
+      "android-studio": android_studio,
+      "clion": clion,
+  }
+
+  # Map (direct ij_product) -> corresponding product directory
+  params = dict()
+  for ij_product, value in DIRECT_IJ_PRODUCTS.items():
+    params[ij_product] = [_plugin_api_directory(value) + item for item in ide_to_value[value.ide]]
+  return select_for_plugin_api(params)
diff --git a/java/src/com/google/idea/blaze/java/sync/BlazeJavaSyncAugmenter.java b/java/src/com/google/idea/blaze/java/sync/BlazeJavaSyncAugmenter.java
index 5b5b23c..7c4aa21 100644
--- a/java/src/com/google/idea/blaze/java/sync/BlazeJavaSyncAugmenter.java
+++ b/java/src/com/google/idea/blaze/java/sync/BlazeJavaSyncAugmenter.java
@@ -16,6 +16,7 @@
 package com.google.idea.blaze.java.sync;
 
 import com.google.idea.blaze.base.ideinfo.TargetIdeInfo;
+import com.google.idea.blaze.base.projectview.ProjectViewSet;
 import com.google.idea.blaze.base.sync.projectview.WorkspaceLanguageSettings;
 import com.google.idea.blaze.java.sync.model.BlazeJarLibrary;
 import com.intellij.openapi.extensions.ExtensionPointName;
@@ -34,6 +35,7 @@
    */
   void addJarsForSourceTarget(
       WorkspaceLanguageSettings workspaceLanguageSettings,
+      ProjectViewSet projectViewSet,
       TargetIdeInfo target,
       Collection<BlazeJarLibrary> jars,
       Collection<BlazeJarLibrary> genJars);
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 dfca78e..65151a6 100644
--- a/java/src/com/google/idea/blaze/java/sync/BlazeJavaSyncPlugin.java
+++ b/java/src/com/google/idea/blaze/java/sync/BlazeJavaSyncPlugin.java
@@ -36,6 +36,7 @@
 import com.google.idea.blaze.base.scope.scopes.TimingScope;
 import com.google.idea.blaze.base.settings.Blaze;
 import com.google.idea.blaze.base.sync.BlazeSyncPlugin;
+import com.google.idea.blaze.base.sync.SourceFolderProvider;
 import com.google.idea.blaze.base.sync.libraries.LibrarySource;
 import com.google.idea.blaze.base.sync.projectview.WorkspaceLanguageSettings;
 import com.google.idea.blaze.base.sync.workspace.ArtifactLocationDecoder;
@@ -52,15 +53,14 @@
 import com.google.idea.blaze.java.sync.model.BlazeJarLibrary;
 import com.google.idea.blaze.java.sync.model.BlazeJavaImportResult;
 import com.google.idea.blaze.java.sync.model.BlazeJavaSyncData;
+import com.google.idea.blaze.java.sync.projectstructure.JavaSourceFolderProvider;
 import com.google.idea.blaze.java.sync.projectstructure.Jdks;
-import com.google.idea.blaze.java.sync.projectstructure.SourceFolderEditor;
 import com.google.idea.blaze.java.sync.workingset.JavaWorkingSet;
 import com.intellij.openapi.application.ApplicationManager;
 import com.intellij.openapi.module.ModuleType;
 import com.intellij.openapi.module.StdModuleTypes;
 import com.intellij.openapi.project.Project;
 import com.intellij.openapi.projectRoots.Sdk;
-import com.intellij.openapi.roots.ContentEntry;
 import com.intellij.openapi.roots.LanguageLevelProjectExtension;
 import com.intellij.openapi.roots.ex.ProjectRootManagerEx;
 import com.intellij.pom.java.LanguageLevel;
@@ -177,23 +177,13 @@
     updateJdk(project, context, projectViewSet, blazeProjectData);
   }
 
+  @Nullable
   @Override
-  public void updateContentEntries(
-      Project project,
-      BlazeContext context,
-      WorkspaceRoot workspaceRoot,
-      ProjectViewSet projectViewSet,
-      BlazeProjectData blazeProjectData,
-      Collection<ContentEntry> contentEntries) {
-    if (!blazeProjectData.workspaceLanguageSettings.isLanguageActive(LanguageClass.JAVA)) {
-      return;
+  public SourceFolderProvider getSourceFolderProvider(BlazeProjectData projectData) {
+    if (!projectData.workspaceLanguageSettings.isWorkspaceType(WorkspaceType.JAVA)) {
+      return null;
     }
-    BlazeJavaSyncData syncData = blazeProjectData.syncState.get(BlazeJavaSyncData.class);
-    if (syncData == null) {
-      return;
-    }
-
-    SourceFolderEditor.modifyContentEntries(syncData.importResult, contentEntries);
+    return new JavaSourceFolderProvider(projectData.syncState.get(BlazeJavaSyncData.class));
   }
 
   private static void updateJdk(
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 80d7d83..2e8df7f 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
@@ -40,7 +40,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.SourceTestConfig;
 import com.google.idea.blaze.base.sync.projectview.WorkspaceLanguageSettings;
 import com.google.idea.blaze.base.sync.workspace.ArtifactLocationDecoder;
 import com.google.idea.blaze.java.sync.BlazeJavaSyncAugmenter;
@@ -67,7 +66,6 @@
   private final WorkspaceRoot workspaceRoot;
   private final ImportRoots importRoots;
   private final TargetMap targetMap;
-  private final SourceTestConfig sourceTestConfig;
   private final JdepsMap jdepsMap;
   @Nullable private final JavaWorkingSet workingSet;
   private final ArtifactLocationDecoder artifactLocationDecoder;
@@ -75,6 +73,7 @@
   private final JavaSourceFilter sourceFilter;
   private final WorkspaceLanguageSettings workspaceLanguageSettings;
   private final List<BlazeJavaSyncAugmenter> augmenters;
+  private final ProjectViewSet projectViewSet;
 
   public BlazeJavaWorkspaceImporter(
       Project project,
@@ -97,9 +96,9 @@
     this.jdepsMap = jdepsMap;
     this.workingSet = workingSet;
     this.artifactLocationDecoder = artifactLocationDecoder;
-    this.sourceTestConfig = new SourceTestConfig(projectViewSet);
     this.workspaceLanguageSettings = workspaceLanguageSettings;
     this.augmenters = Arrays.asList(BlazeJavaSyncAugmenter.EP_NAME.getExtensions());
+    this.projectViewSet = projectViewSet;
   }
 
   public BlazeJavaImportResult importWorkspace(BlazeContext context) {
@@ -114,7 +113,6 @@
             project,
             context,
             workspaceRoot,
-            sourceTestConfig,
             artifactLocationDecoder,
             importRoots.rootDirectories(),
             workspaceBuilder.sourceArtifacts,
@@ -389,6 +387,7 @@
     for (BlazeJavaSyncAugmenter augmenter : augmenters) {
       augmenter.addJarsForSourceTarget(
           workspaceLanguageSettings,
+          projectViewSet,
           target,
           workspaceBuilder.outputJarsFromSourceTargets.get(targetKey),
           workspaceBuilder.generatedJarsFromSourceTargets);
diff --git a/java/src/com/google/idea/blaze/java/sync/model/BlazeSourceDirectory.java b/java/src/com/google/idea/blaze/java/sync/model/BlazeSourceDirectory.java
index f39a2e0..dd7db2b 100644
--- a/java/src/com/google/idea/blaze/java/sync/model/BlazeSourceDirectory.java
+++ b/java/src/com/google/idea/blaze/java/sync/model/BlazeSourceDirectory.java
@@ -20,46 +20,38 @@
 import java.io.Serializable;
 import java.util.Comparator;
 import javax.annotation.concurrent.Immutable;
-import org.jetbrains.annotations.NotNull;
 
 /** A source directory. */
 @Immutable
 public final class BlazeSourceDirectory implements Serializable {
-  private static final long serialVersionUID = 2L;
+  private static final long serialVersionUID = 3L;
 
   public static final Comparator<BlazeSourceDirectory> COMPARATOR =
       (o1, o2) ->
           String.CASE_INSENSITIVE_ORDER.compare(
               o1.getDirectory().getPath(), o2.getDirectory().getPath());
 
-  @NotNull private final File directory;
-  private final boolean isTest;
+  private final File directory;
   private final boolean isGenerated;
   private final boolean isResource;
-  @NotNull private final String packagePrefix;
+  private final String packagePrefix;
 
   /** Bulider for source directory */
   public static class Builder {
-    @NotNull private final File directory;
-    @NotNull private String packagePrefix = "";
-    private boolean isTest;
+    private final File directory;
+    private String packagePrefix = "";
     private boolean isResource;
     private boolean isGenerated;
 
-    private Builder(@NotNull File directory) {
+    private Builder(File directory) {
       this.directory = directory;
     }
 
-    public Builder setPackagePrefix(@NotNull String packagePrefix) {
+    public Builder setPackagePrefix(String packagePrefix) {
       this.packagePrefix = packagePrefix;
       return this;
     }
 
-    public Builder setTest(boolean isTest) {
-      this.isTest = isTest;
-      return this;
-    }
-
     public Builder setResource(boolean isResource) {
       this.isResource = isResource;
       return this;
@@ -71,44 +63,31 @@
     }
 
     public BlazeSourceDirectory build() {
-      return new BlazeSourceDirectory(directory, isTest, isResource, isGenerated, packagePrefix);
+      return new BlazeSourceDirectory(directory, isResource, isGenerated, packagePrefix);
     }
   }
 
-  @NotNull
-  public static Builder builder(@NotNull String directory) {
+  public static Builder builder(String directory) {
     return new Builder(new File(directory));
   }
 
-  @NotNull
-  public static Builder builder(@NotNull File directory) {
+  public static Builder builder(File directory) {
     return new Builder(directory);
   }
 
   private BlazeSourceDirectory(
-      @NotNull File directory,
-      boolean isTest,
-      boolean isResource,
-      boolean isGenerated,
-      @NotNull String packagePrefix) {
+      File directory, boolean isResource, boolean isGenerated, String packagePrefix) {
     this.directory = directory;
-    this.isTest = isTest;
     this.isResource = isResource;
     this.isGenerated = isGenerated;
     this.packagePrefix = packagePrefix;
   }
 
   /** Returns the full path name of the root of a source directory. */
-  @NotNull
   public File getDirectory() {
     return directory;
   }
 
-  /** Returns {@code true} if the directory contains test sources. */
-  public boolean getIsTest() {
-    return isTest;
-  }
-
   /** Returns {@code true} if the directory contains resources. */
   public boolean getIsResource() {
     return isResource;
@@ -123,14 +102,13 @@
    * Returns the package prefix for the directory. If the directory is a source root, such as a
    * "src" directory, then this returns an empty string.
    */
-  @NotNull
   public String getPackagePrefix() {
     return packagePrefix;
   }
 
   @Override
   public int hashCode() {
-    return Objects.hashCode(directory, isTest, isResource, packagePrefix, isGenerated);
+    return Objects.hashCode(directory, isResource, packagePrefix, isGenerated);
   }
 
   @Override
@@ -145,7 +123,6 @@
     return directory.equals(that.directory)
         && packagePrefix.equals(that.packagePrefix)
         && isResource == that.isResource
-        && isTest == that.isTest
         && isGenerated == that.isGenerated;
   }
 
@@ -155,9 +132,6 @@
         + "  directory: "
         + directory
         + "\n"
-        + "  isTest: "
-        + isTest
-        + "\n"
         + "  isGenerated: "
         + isGenerated
         + "\n"
diff --git a/java/src/com/google/idea/blaze/java/sync/projectstructure/JavaSourceFolderProvider.java b/java/src/com/google/idea/blaze/java/sync/projectstructure/JavaSourceFolderProvider.java
new file mode 100644
index 0000000..5855655
--- /dev/null
+++ b/java/src/com/google/idea/blaze/java/sync/projectstructure/JavaSourceFolderProvider.java
@@ -0,0 +1,173 @@
+/*
+ * 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.projectstructure;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.base.Strings;
+import com.google.common.collect.ImmutableMap;
+import com.google.idea.blaze.base.sync.SourceFolderProvider;
+import com.google.idea.blaze.java.sync.model.BlazeContentEntry;
+import com.google.idea.blaze.java.sync.model.BlazeJavaSyncData;
+import com.google.idea.blaze.java.sync.model.BlazeSourceDirectory;
+import com.intellij.openapi.application.ApplicationManager;
+import com.intellij.openapi.roots.ContentEntry;
+import com.intellij.openapi.roots.SourceFolder;
+import com.intellij.openapi.util.io.FileUtil;
+import com.intellij.openapi.vfs.LocalFileSystem;
+import com.intellij.openapi.vfs.VfsUtilCore;
+import com.intellij.openapi.vfs.VirtualFile;
+import com.intellij.openapi.vfs.VirtualFileManager;
+import com.intellij.openapi.vfs.VirtualFileSystem;
+import com.intellij.openapi.vfs.ex.temp.TempFileSystem;
+import com.intellij.util.io.URLUtil;
+import java.io.File;
+import javax.annotation.Nullable;
+import org.jetbrains.jps.model.JpsElement;
+import org.jetbrains.jps.model.java.JavaResourceRootType;
+import org.jetbrains.jps.model.java.JavaSourceRootProperties;
+import org.jetbrains.jps.model.module.JpsModuleSourceRoot;
+
+/** Edits source folders in IntelliJ content entries */
+public class JavaSourceFolderProvider implements SourceFolderProvider {
+
+  private final ImmutableMap<File, BlazeContentEntry> blazeContentEntries;
+
+  public JavaSourceFolderProvider(@Nullable BlazeJavaSyncData syncData) {
+    this.blazeContentEntries = blazeContentEntries(syncData);
+  }
+
+  private static ImmutableMap<File, BlazeContentEntry> blazeContentEntries(
+      @Nullable BlazeJavaSyncData syncData) {
+    if (syncData == null) {
+      return ImmutableMap.of();
+    }
+    ImmutableMap.Builder<File, BlazeContentEntry> builder = ImmutableMap.builder();
+    for (BlazeContentEntry blazeContentEntry : syncData.importResult.contentEntries) {
+      builder.put(blazeContentEntry.contentRoot, blazeContentEntry);
+    }
+    return builder.build();
+  }
+
+  @Override
+  public ImmutableMap<VirtualFile, SourceFolder> initializeSourceFolders(
+      ContentEntry contentEntry) {
+    ImmutableMap.Builder<VirtualFile, SourceFolder> output = ImmutableMap.builder();
+    VirtualFile virtualFile = contentEntry.getFile();
+    if (virtualFile == null) {
+      return output.build();
+    }
+
+    File contentRoot = new File(virtualFile.getPath());
+    BlazeContentEntry javaContentEntry = blazeContentEntries.get(contentRoot);
+    if (javaContentEntry != null) {
+      for (BlazeSourceDirectory sourceDirectory : javaContentEntry.sources) {
+        SourceFolder sourceFolder = addSourceFolderToContentEntry(contentEntry, sourceDirectory);
+        output.put(sourceFolder.getFile(), sourceFolder);
+      }
+    }
+    return output.build();
+  }
+
+  @Override
+  public SourceFolder setSourceFolderForLocation(
+      ContentEntry contentEntry,
+      SourceFolder parentFolder,
+      VirtualFile file,
+      boolean isTestSource) {
+    SourceFolder sourceFolder;
+    if (isResource(parentFolder)) {
+      JavaResourceRootType resourceRootType =
+          isTestSource ? JavaResourceRootType.TEST_RESOURCE : JavaResourceRootType.RESOURCE;
+      sourceFolder = contentEntry.addSourceFolder(pathToUrl(file.getPath()), resourceRootType);
+    } else {
+      sourceFolder = contentEntry.addSourceFolder(pathToUrl(file.getPath()), isTestSource);
+    }
+    sourceFolder.setPackagePrefix(derivePackagePrefix(file, parentFolder));
+    JpsModuleSourceRoot sourceRoot = sourceFolder.getJpsElement();
+    JpsElement properties = sourceRoot.getProperties();
+    if (properties instanceof JavaSourceRootProperties) {
+      ((JavaSourceRootProperties) properties).setForGeneratedSources(isGenerated(parentFolder));
+    }
+    return sourceFolder;
+  }
+
+  private static String derivePackagePrefix(VirtualFile file, SourceFolder parentFolder) {
+    String parentPackagePrefix = parentFolder.getPackagePrefix();
+    String relativePath = VfsUtilCore.getRelativePath(file, parentFolder.getFile(), '.');
+    if (Strings.isNullOrEmpty(relativePath)) {
+      return parentPackagePrefix;
+    }
+    return parentPackagePrefix + "." + relativePath;
+  }
+
+  @VisibleForTesting
+  static boolean isResource(SourceFolder folder) {
+    return folder.getRootType() instanceof JavaResourceRootType;
+  }
+
+  @VisibleForTesting
+  static boolean isGenerated(SourceFolder folder) {
+    JpsElement properties = folder.getJpsElement().getProperties();
+    return properties instanceof JavaSourceRootProperties
+        && ((JavaSourceRootProperties) properties).isForGeneratedSources();
+  }
+
+  private static SourceFolder addSourceFolderToContentEntry(
+      ContentEntry contentEntry, BlazeSourceDirectory sourceDirectory) {
+    File sourceDir = sourceDirectory.getDirectory();
+
+    // Create the source folder
+    SourceFolder sourceFolder;
+    if (sourceDirectory.getIsResource()) {
+      sourceFolder =
+          contentEntry.addSourceFolder(
+              pathToUrl(sourceDir.getPath()), JavaResourceRootType.RESOURCE);
+    } else {
+      sourceFolder = contentEntry.addSourceFolder(pathToUrl(sourceDir.getPath()), false);
+    }
+    JpsModuleSourceRoot sourceRoot = sourceFolder.getJpsElement();
+    JpsElement properties = sourceRoot.getProperties();
+    if (properties instanceof JavaSourceRootProperties) {
+      JavaSourceRootProperties rootProperties = (JavaSourceRootProperties) properties;
+      if (sourceDirectory.getIsGenerated()) {
+        rootProperties.setForGeneratedSources(true);
+      }
+    }
+    String packagePrefix = sourceDirectory.getPackagePrefix();
+    if (!Strings.isNullOrEmpty(packagePrefix)) {
+      sourceFolder.setPackagePrefix(packagePrefix);
+    }
+    return sourceFolder;
+  }
+
+  private static String pathToUrl(String filePath) {
+    filePath = FileUtil.toSystemIndependentName(filePath);
+    if (filePath.endsWith(".srcjar") || filePath.endsWith(".jar")) {
+      return URLUtil.JAR_PROTOCOL + URLUtil.SCHEME_SEPARATOR + filePath + URLUtil.JAR_SEPARATOR;
+    } else if (filePath.contains("src.jar!")) {
+      return URLUtil.JAR_PROTOCOL + URLUtil.SCHEME_SEPARATOR + filePath;
+    } else {
+      return VirtualFileManager.constructUrl(defaultFileSystem().getProtocol(), filePath);
+    }
+  }
+
+  private static VirtualFileSystem defaultFileSystem() {
+    if (ApplicationManager.getApplication().isUnitTestMode()) {
+      return TempFileSystem.getInstance();
+    }
+    return LocalFileSystem.getInstance();
+  }
+}
diff --git a/java/src/com/google/idea/blaze/java/sync/projectstructure/SourceFolderEditor.java b/java/src/com/google/idea/blaze/java/sync/projectstructure/SourceFolderEditor.java
deleted file mode 100644
index 0d88c4e..0000000
--- a/java/src/com/google/idea/blaze/java/sync/projectstructure/SourceFolderEditor.java
+++ /dev/null
@@ -1,108 +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.java.sync.projectstructure;
-
-import com.google.common.base.Strings;
-import com.google.common.collect.Maps;
-import com.google.idea.blaze.java.sync.model.BlazeContentEntry;
-import com.google.idea.blaze.java.sync.model.BlazeJavaImportResult;
-import com.google.idea.blaze.java.sync.model.BlazeSourceDirectory;
-import com.intellij.openapi.diagnostic.Logger;
-import com.intellij.openapi.roots.ContentEntry;
-import com.intellij.openapi.roots.SourceFolder;
-import com.intellij.openapi.util.io.FileUtil;
-import com.intellij.openapi.vfs.VfsUtilCore;
-import com.intellij.openapi.vfs.VirtualFile;
-import com.intellij.util.io.URLUtil;
-import java.io.File;
-import java.util.Collection;
-import java.util.Map;
-import org.jetbrains.annotations.NotNull;
-import org.jetbrains.jps.model.JpsElement;
-import org.jetbrains.jps.model.java.JavaResourceRootType;
-import org.jetbrains.jps.model.java.JavaSourceRootProperties;
-import org.jetbrains.jps.model.module.JpsModuleSourceRoot;
-
-/** Edits source folders in IntelliJ content entries */
-public class SourceFolderEditor {
-  private static final Logger LOG = Logger.getInstance(SourceFolderEditor.class);
-
-  public static void modifyContentEntries(
-      BlazeJavaImportResult importResult, Collection<ContentEntry> contentEntries) {
-
-    Map<File, BlazeContentEntry> contentEntryMap = Maps.newHashMap();
-    for (BlazeContentEntry contentEntry : importResult.contentEntries) {
-      contentEntryMap.put(contentEntry.contentRoot, contentEntry);
-    }
-
-    for (ContentEntry contentEntry : contentEntries) {
-      VirtualFile virtualFile = contentEntry.getFile();
-      if (virtualFile == null) {
-        continue;
-      }
-
-      File contentRoot = new File(virtualFile.getPath());
-      BlazeContentEntry javaContentEntry = contentEntryMap.get(contentRoot);
-      if (javaContentEntry != null) {
-        for (BlazeSourceDirectory sourceDirectory : javaContentEntry.sources) {
-          addSourceFolderToContentEntry(contentEntry, sourceDirectory);
-        }
-      }
-    }
-  }
-
-  private static void addSourceFolderToContentEntry(
-      ContentEntry contentEntry, BlazeSourceDirectory sourceDirectory) {
-    File sourceDir = sourceDirectory.getDirectory();
-
-    // Create the source folder
-    SourceFolder sourceFolder;
-    if (sourceDirectory.getIsResource()) {
-      JavaResourceRootType resourceRootType =
-          sourceDirectory.getIsTest()
-              ? JavaResourceRootType.TEST_RESOURCE
-              : JavaResourceRootType.RESOURCE;
-      sourceFolder = contentEntry.addSourceFolder(pathToUrl(sourceDir.getPath()), resourceRootType);
-    } else {
-      sourceFolder =
-          contentEntry.addSourceFolder(pathToUrl(sourceDir.getPath()), sourceDirectory.getIsTest());
-    }
-    JpsModuleSourceRoot sourceRoot = sourceFolder.getJpsElement();
-    JpsElement properties = sourceRoot.getProperties();
-    if (properties instanceof JavaSourceRootProperties) {
-      JavaSourceRootProperties rootProperties = (JavaSourceRootProperties) properties;
-      if (sourceDirectory.getIsGenerated()) {
-        rootProperties.setForGeneratedSources(true);
-      }
-      String packagePrefix = sourceDirectory.getPackagePrefix();
-      if (!Strings.isNullOrEmpty(packagePrefix)) {
-        rootProperties.setPackagePrefix(packagePrefix);
-      }
-    }
-  }
-
-  @NotNull
-  private static String pathToUrl(@NotNull String filePath) {
-    filePath = FileUtil.toSystemIndependentName(filePath);
-    if (filePath.endsWith(".srcjar") || filePath.endsWith(".jar")) {
-      return URLUtil.JAR_PROTOCOL + URLUtil.SCHEME_SEPARATOR + filePath + URLUtil.JAR_SEPARATOR;
-    } else if (filePath.contains("src.jar!")) {
-      return URLUtil.JAR_PROTOCOL + URLUtil.SCHEME_SEPARATOR + filePath;
-    } else {
-      return VfsUtilCore.pathToUrl(filePath);
-    }
-  }
-}
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 805dc21..133625e 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
@@ -41,7 +41,6 @@
 import com.google.idea.blaze.base.scope.Scope;
 import com.google.idea.blaze.base.scope.output.IssueOutput;
 import com.google.idea.blaze.base.scope.scopes.TimingScope;
-import com.google.idea.blaze.base.sync.projectview.SourceTestConfig;
 import com.google.idea.blaze.base.sync.workspace.ArtifactLocationDecoder;
 import com.google.idea.blaze.base.util.PackagePrefixCalculator;
 import com.google.idea.blaze.java.sync.model.BlazeContentEntry;
@@ -81,7 +80,6 @@
       Project project,
       BlazeContext context,
       WorkspaceRoot workspaceRoot,
-      SourceTestConfig sourceTestConfig,
       ArtifactLocationDecoder artifactLocationDecoder,
       Collection<WorkspacePath> rootDirectories,
       Collection<SourceArtifact> sources,
@@ -127,7 +125,6 @@
                     context,
                     workspaceRoot,
                     artifactLocationDecoder,
-                    sourceTestConfig,
                     workspacePath,
                     sourcesUnderDirectoryRoot.get(workspacePath),
                     javaPackageReaders);
@@ -196,7 +193,6 @@
       BlazeContext context,
       WorkspaceRoot workspaceRoot,
       ArtifactLocationDecoder artifactLocationDecoder,
-      SourceTestConfig sourceTestConfig,
       WorkspacePath directoryRoot,
       Collection<SourceArtifact> sourceArtifacts,
       Collection<JavaPackageReader> javaPackageReaders) {
@@ -217,7 +213,6 @@
         workspaceRoot,
         artifactLocationDecoder,
         directoryRoot,
-        sourceTestConfig,
         javaArtifacts,
         javaPackageReaders,
         result);
@@ -232,7 +227,6 @@
       WorkspaceRoot workspaceRoot,
       ArtifactLocationDecoder artifactLocationDecoder,
       WorkspacePath directoryRoot,
-      SourceTestConfig sourceTestConfig,
       Collection<SourceArtifact> javaArtifacts,
       Collection<JavaPackageReader> javaPackageReaders,
       Collection<BlazeSourceDirectory> result) {
@@ -368,7 +362,6 @@
       result.add(
           BlazeSourceDirectory.builder(workspaceRoot.fileForPath(sourceRoot.workspacePath))
               .setPackagePrefix(sourceRoot.packagePrefix)
-              .setTest(sourceTestConfig.isTestSource(sourceRoot.workspacePath.relativePath()))
               .setGenerated(false)
               .build());
     }
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 352553b..ff6365c 100644
--- a/java/src/com/google/idea/blaze/java/syncstatus/SyncStatusHelper.java
+++ b/java/src/com/google/idea/blaze/java/syncstatus/SyncStatusHelper.java
@@ -20,6 +20,7 @@
 import com.google.idea.blaze.base.projectview.ProjectViewSet;
 import com.google.idea.blaze.base.scope.BlazeContext;
 import com.google.idea.blaze.base.settings.BlazeImportSettings;
+import com.google.idea.blaze.base.sync.BlazeSyncParams.SyncMode;
 import com.google.idea.blaze.base.sync.SyncListener;
 import com.google.idea.blaze.base.sync.data.BlazeProjectDataManager;
 import com.google.idea.blaze.base.sync.workspace.ArtifactLocationDecoder;
@@ -85,6 +86,7 @@
         BlazeImportSettings importSettings,
         ProjectViewSet projectViewSet,
         BlazeProjectData blazeProjectData,
+        SyncMode syncMode,
         SyncResult syncResult) {
       getInstance(project).syncedJavaFiles = null;
     }
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 074cd27..a87565e 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
@@ -17,6 +17,7 @@
 
 import static com.google.common.truth.Truth.assertThat;
 
+import com.google.common.collect.ImmutableList;
 import com.google.idea.blaze.base.ideinfo.JavaIdeInfo;
 import com.google.idea.blaze.base.ideinfo.TargetIdeInfo;
 import com.google.idea.blaze.base.ideinfo.TargetMap;
@@ -31,7 +32,11 @@
 import com.google.idea.blaze.java.sync.model.BlazeContentEntry;
 import com.google.idea.blaze.java.sync.model.BlazeJavaSyncData;
 import com.google.idea.blaze.java.sync.model.BlazeSourceDirectory;
+import com.intellij.openapi.roots.ContentEntry;
+import com.intellij.openapi.roots.SourceFolder;
+import com.intellij.openapi.vfs.VirtualFile;
 import java.util.List;
+import javax.annotation.Nullable;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.junit.runners.JUnit4;
@@ -42,12 +47,7 @@
 
   @Test
   public void testJavaClassesPresentInClassPath() throws Exception {
-    setProjectView(
-        "directories:",
-        "  java/com/google",
-        "targets:",
-        "  //java/com/google:lib",
-        "workspace_type: java");
+    setProjectView("directories:", "  java/com/google", "targets:", "  //java/com/google:lib");
 
     workspace.createFile(
         new WorkspacePath("java/com/google/ClassWithUniqueName1.java"),
@@ -105,12 +105,7 @@
 
   @Test
   public void testSimpleSync() throws Exception {
-    setProjectView(
-        "directories:",
-        "  java/com/google",
-        "targets:",
-        "  //java/com/google:lib",
-        "workspace_type: java");
+    setProjectView("directories:", "  java/com/google", "targets:", "  //java/com/google:lib");
 
     workspace.createFile(
         new WorkspacePath("java/com/google/Source.java"),
@@ -149,4 +144,195 @@
     assertThat(blazeProjectData.workspaceLanguageSettings.getWorkspaceType())
         .isEqualTo(WorkspaceType.JAVA);
   }
+
+  @Test
+  public void testSimpleTestSourcesIdentified() {
+    setProjectView(
+        "directories:",
+        "  java/com/google",
+        "  javatests/com/google",
+        "targets:",
+        "  //java/com/google:lib",
+        "test_sources:",
+        "  javatests/*");
+
+    VirtualFile javaRoot = workspace.createDirectory(new WorkspacePath("java/com/google"));
+    VirtualFile javatestsRoot =
+        workspace.createDirectory(new WorkspacePath("javatests/com/google"));
+
+    BlazeSyncParams syncParams =
+        new BlazeSyncParams.Builder("Full Sync", BlazeSyncParams.SyncMode.FULL)
+            .addProjectViewTargets(true)
+            .build();
+    runBlazeSync(syncParams);
+
+    errorCollector.assertNoIssues();
+
+    ImmutableList<ContentEntry> contentEntries = getWorkspaceContentEntries();
+    assertThat(contentEntries).hasSize(2);
+
+    assertThat(findContentEntry(javaRoot)).isNotNull();
+    assertThat(findContentEntry(javaRoot).getSourceFolders()).hasLength(1);
+    assertThat(findContentEntry(javaRoot).getSourceFolders()[0].isTestSource()).isFalse();
+
+    assertThat(findContentEntry(javatestsRoot)).isNotNull();
+    assertThat(findContentEntry(javatestsRoot).getSourceFolders()).hasLength(1);
+    assertThat(findContentEntry(javatestsRoot).getSourceFolders()[0].isTestSource()).isTrue();
+  }
+
+  @Test
+  public void testNestedTestSourcesAreAdded() {
+    setProjectView(
+        "directories:",
+        "  java/com/google",
+        "targets:",
+        "  //java/com/google:lib",
+        "test_sources:",
+        "  java/com/google/tests/*",
+        "  java/com/google/moretests");
+
+    workspace.createDirectory(new WorkspacePath("java/com/google"));
+
+    VirtualFile testFile =
+        workspace.createFile(new WorkspacePath("java/com/google/tests/ExampleTest.java"));
+    VirtualFile moreTestsFile =
+        workspace.createFile(
+            new WorkspacePath("java/com/google/moretests/AnotherExampleTest.java"));
+
+    BlazeSyncParams syncParams =
+        new BlazeSyncParams.Builder("Full Sync", BlazeSyncParams.SyncMode.FULL)
+            .addProjectViewTargets(true)
+            .build();
+    runBlazeSync(syncParams);
+
+    errorCollector.assertNoIssues();
+
+    ImmutableList<ContentEntry> contentEntries = getWorkspaceContentEntries();
+    assertThat(contentEntries).hasSize(1);
+
+    SourceFolder testRoot = findSourceFolder(contentEntries.get(0), testFile.getParent());
+    SourceFolder moreTestsRoot = findSourceFolder(contentEntries.get(0), moreTestsFile.getParent());
+
+    assertThat(testRoot).isNotNull();
+    assertThat(testRoot.isTestSource()).isTrue();
+    assertThat(testRoot.getPackagePrefix()).isEqualTo("com.google.tests");
+
+    assertThat(moreTestsRoot).isNotNull();
+    assertThat(moreTestsRoot.isTestSource()).isTrue();
+    assertThat(moreTestsRoot.getPackagePrefix()).isEqualTo("com.google.moretests");
+  }
+
+  @Test
+  public void testTestSourcesUpdateCorrectlyOnSubsequentSync() {
+    setProjectView(
+        "directories:",
+        "  java/com/google",
+        "targets:",
+        "  //java/com/google:lib",
+        "test_sources:",
+        "  java/com/google/tests/*");
+
+    VirtualFile root = workspace.createDirectory(new WorkspacePath("java/com/google"));
+
+    VirtualFile testsDir = workspace.createDirectory(new WorkspacePath("java/com/google/tests"));
+    VirtualFile moreTestsDir =
+        workspace.createDirectory(new WorkspacePath("java/com/google/moretests"));
+
+    BlazeSyncParams syncParams =
+        new BlazeSyncParams.Builder("Full Sync", BlazeSyncParams.SyncMode.FULL)
+            .addProjectViewTargets(true)
+            .build();
+    runBlazeSync(syncParams);
+
+    errorCollector.assertNoIssues();
+
+    ContentEntry contentEntry = findContentEntry(root);
+    assertThat(findSourceFolder(contentEntry, testsDir).isTestSource()).isTrue();
+    assertThat(findSourceFolder(contentEntry, moreTestsDir)).isNull();
+
+    // unmark one test source, mark another.
+    setProjectView(
+        "directories:",
+        "  java/com/google",
+        "targets:",
+        "  //java/com/google:lib",
+        "test_sources:",
+        "  java/com/google/moretests/*");
+
+    runBlazeSync(syncParams);
+
+    contentEntry = findContentEntry(root);
+    assertThat(findSourceFolder(contentEntry, testsDir)).isNull();
+    assertThat(findSourceFolder(contentEntry, moreTestsDir).isTestSource()).isTrue();
+  }
+
+  @Test
+  public void testExistingPackagePrefixRetainedForTestSources() {
+    setProjectView(
+        "directories:",
+        "  java/com/google",
+        "  javatests/com/google",
+        "targets:",
+        "  //java/com/google:lib",
+        "test_sources:",
+        "  javatests/*");
+
+    workspace.createDirectory(new WorkspacePath("java/com/google"));
+    VirtualFile javatestsRoot =
+        workspace.createDirectory(new WorkspacePath("javatests/com/google"));
+
+    BlazeSyncParams syncParams =
+        new BlazeSyncParams.Builder("Full Sync", BlazeSyncParams.SyncMode.FULL)
+            .addProjectViewTargets(true)
+            .build();
+    runBlazeSync(syncParams);
+
+    errorCollector.assertNoIssues();
+
+    ContentEntry testRoot = findContentEntry(javatestsRoot);
+    SourceFolder testRootSource = findSourceFolder(testRoot, javatestsRoot);
+    assertThat(testRootSource.isTestSource()).isTrue();
+    assertThat(testRootSource.getPackagePrefix()).isEqualTo("com.google");
+  }
+
+  @Test
+  public void testTestSourceRelativePackagePrefixCalculation() {
+    setProjectView(
+        "directories:",
+        "  java/com/google",
+        "targets:",
+        "  //java/com/google:lib",
+        "test_sources:",
+        "  java/com/google/tests/*");
+
+    VirtualFile javatestsRoot =
+        workspace.createDirectory(new WorkspacePath("java/com/google/tests"));
+
+    BlazeSyncParams syncParams =
+        new BlazeSyncParams.Builder("Full Sync", BlazeSyncParams.SyncMode.FULL)
+            .addProjectViewTargets(true)
+            .build();
+    runBlazeSync(syncParams);
+
+    errorCollector.assertNoIssues();
+
+    ContentEntry root = findContentEntry(javatestsRoot.getParent());
+    SourceFolder rootSource = findSourceFolder(root, javatestsRoot.getParent());
+    assertThat(rootSource.isTestSource()).isFalse();
+    assertThat(rootSource.getPackagePrefix()).isEqualTo("com.google");
+
+    SourceFolder childTestSource = findSourceFolder(root, javatestsRoot);
+    assertThat(childTestSource.isTestSource()).isTrue();
+    assertThat(childTestSource.getPackagePrefix()).isEqualTo("com.google.tests");
+  }
+
+  @Nullable
+  private static SourceFolder findSourceFolder(ContentEntry entry, VirtualFile file) {
+    for (SourceFolder sourceFolder : entry.getSourceFolders()) {
+      if (file.equals(sourceFolder.getFile())) {
+        return sourceFolder;
+      }
+    }
+    return null;
+  }
 }
diff --git a/java/tests/integrationtests/com/google/idea/blaze/java/sync/projectstructure/JavaSourceFolderProviderTest.java b/java/tests/integrationtests/com/google/idea/blaze/java/sync/projectstructure/JavaSourceFolderProviderTest.java
new file mode 100644
index 0000000..73e67a3
--- /dev/null
+++ b/java/tests/integrationtests/com/google/idea/blaze/java/sync/projectstructure/JavaSourceFolderProviderTest.java
@@ -0,0 +1,143 @@
+/*
+ * 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.projectstructure;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.ImmutableSet;
+import com.google.idea.blaze.base.BlazeIntegrationTestCase;
+import com.google.idea.blaze.base.model.primitives.WorkspacePath;
+import com.google.idea.blaze.base.projectview.section.Glob.GlobSet;
+import com.google.idea.blaze.java.sync.model.BlazeContentEntry;
+import com.google.idea.blaze.java.sync.model.BlazeJavaImportResult;
+import com.google.idea.blaze.java.sync.model.BlazeJavaSyncData;
+import com.google.idea.blaze.java.sync.model.BlazeSourceDirectory;
+import com.intellij.openapi.roots.ContentEntry;
+import com.intellij.openapi.roots.ModuleRootManager;
+import com.intellij.openapi.roots.SourceFolder;
+import com.intellij.openapi.vfs.VirtualFile;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** Tests for {@link JavaSourceFolderProvider} */
+@RunWith(JUnit4.class)
+public class JavaSourceFolderProviderTest extends BlazeIntegrationTestCase {
+
+  @Test
+  public void testInitializeSourceFolders() {
+    ImmutableList<BlazeContentEntry> contentEntries =
+        ImmutableList.of(
+            BlazeContentEntry.builder("/src/workspace/java/apps")
+                .addSource(
+                    BlazeSourceDirectory.builder("/src/workspace/java/apps")
+                        .setPackagePrefix("apps")
+                        .build())
+                .addSource(
+                    BlazeSourceDirectory.builder("/src/workspace/java/apps/gen")
+                        .setPackagePrefix("apps.gen")
+                        .setGenerated(true)
+                        .build())
+                .addSource(
+                    BlazeSourceDirectory.builder("/src/workspace/java/apps/resources")
+                        .setPackagePrefix("apps.resources")
+                        .setResource(true)
+                        .build())
+                .build(),
+            BlazeContentEntry.builder("/src/workspace/javatests/apps/example")
+                .addSource(
+                    BlazeSourceDirectory.builder("/src/workspace/javatests/apps/example")
+                        .setPackagePrefix("apps.example")
+                        .build())
+                .build());
+
+    JavaSourceFolderProvider provider =
+        new JavaSourceFolderProvider(
+            new BlazeJavaSyncData(
+                new BlazeJavaImportResult(
+                    contentEntries, ImmutableMap.of(), ImmutableList.of(), ImmutableSet.of(), null),
+                new GlobSet(ImmutableList.of())));
+
+    VirtualFile root = workspace.createDirectory(new WorkspacePath("java/apps"));
+    VirtualFile gen = workspace.createDirectory(new WorkspacePath("java/apps/gen"));
+    VirtualFile res = workspace.createDirectory(new WorkspacePath("java/apps/resources"));
+
+    ImmutableMap<VirtualFile, SourceFolder> sourceFolders =
+        provider.initializeSourceFolders(getContentEntry(root));
+    assertThat(sourceFolders).hasSize(3);
+
+    SourceFolder rootSource = sourceFolders.get(root);
+    assertThat(rootSource.getPackagePrefix()).isEqualTo("apps");
+    assertThat(JavaSourceFolderProvider.isGenerated(rootSource)).isFalse();
+    assertThat(JavaSourceFolderProvider.isResource(rootSource)).isFalse();
+
+    SourceFolder genSource = sourceFolders.get(gen);
+    assertThat(genSource.getPackagePrefix()).isEqualTo("apps.gen");
+    assertThat(JavaSourceFolderProvider.isGenerated(genSource)).isTrue();
+    assertThat(JavaSourceFolderProvider.isResource(genSource)).isFalse();
+
+    SourceFolder resSource = sourceFolders.get(res);
+    assertThat(JavaSourceFolderProvider.isGenerated(resSource)).isFalse();
+    assertThat(JavaSourceFolderProvider.isResource(resSource)).isTrue();
+
+    VirtualFile testRoot = workspace.createDirectory(new WorkspacePath("javatests/apps/example"));
+    sourceFolders = provider.initializeSourceFolders(getContentEntry(testRoot));
+
+    assertThat(sourceFolders).hasSize(1);
+    assertThat(sourceFolders.get(testRoot).getPackagePrefix()).isEqualTo("apps.example");
+  }
+
+  @Test
+  public void testRelativePackagePrefix() {
+    ImmutableList<BlazeContentEntry> contentEntries =
+        ImmutableList.of(
+            BlazeContentEntry.builder("/src/workspace/java/apps")
+                .addSource(
+                    BlazeSourceDirectory.builder("/src/workspace/java/apps")
+                        .setPackagePrefix("apps")
+                        .build())
+                .build());
+
+    JavaSourceFolderProvider provider =
+        new JavaSourceFolderProvider(
+            new BlazeJavaSyncData(
+                new BlazeJavaImportResult(
+                    contentEntries, ImmutableMap.of(), ImmutableList.of(), ImmutableSet.of(), null),
+                new GlobSet(ImmutableList.of())));
+
+    VirtualFile root = workspace.createDirectory(new WorkspacePath("java/apps"));
+    ContentEntry contentEntry = getContentEntry(root);
+
+    ImmutableMap<VirtualFile, SourceFolder> sourceFolders =
+        provider.initializeSourceFolders(contentEntry);
+    assertThat(sourceFolders).hasSize(1);
+
+    VirtualFile testRoot = workspace.createDirectory(new WorkspacePath("java/apps/tests"));
+
+    SourceFolder testSourceChild =
+        provider.setSourceFolderForLocation(contentEntry, sourceFolders.get(root), testRoot, true);
+    assertThat(testSourceChild.isTestSource()).isTrue();
+    assertThat(testSourceChild.getPackagePrefix()).isEqualTo("apps.tests");
+  }
+
+  private ContentEntry getContentEntry(VirtualFile root) {
+    return ModuleRootManager.getInstance(testFixture.getModule())
+        .getModifiableModel()
+        .addContentEntry(root);
+  }
+}
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 76d49ac..ad731d7 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
@@ -441,7 +441,6 @@
                 .addSource(
                     BlazeSourceDirectory.builder("/root/javatests/apps/example")
                         .setPackagePrefix("apps.example")
-                        .setTest(true)
                         .build())
                 .build());
   }
@@ -562,7 +561,6 @@
                 .addSource(
                     BlazeSourceDirectory.builder("/root/javatests/apps/example")
                         .setPackagePrefix("apps.example")
-                        .setTest(true)
                         .build())
                 .build());
   }
@@ -1330,6 +1328,7 @@
           @Override
           public void addJarsForSourceTarget(
               WorkspaceLanguageSettings workspaceLanguageSettings,
+              ProjectViewSet projectViewSet,
               TargetIdeInfo target,
               Collection<BlazeJarLibrary> jars,
               Collection<BlazeJarLibrary> genJars) {
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 ccd035a..cd5b051 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
@@ -34,11 +34,9 @@
 import com.google.idea.blaze.base.model.primitives.WorkspaceRoot;
 import com.google.idea.blaze.base.prefetch.MockPrefetchService;
 import com.google.idea.blaze.base.prefetch.PrefetchService;
-import com.google.idea.blaze.base.projectview.ProjectViewSet;
 import com.google.idea.blaze.base.scope.BlazeContext;
 import com.google.idea.blaze.base.scope.ErrorCollector;
 import com.google.idea.blaze.base.scope.output.IssueOutput;
-import com.google.idea.blaze.base.sync.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;
@@ -82,20 +80,6 @@
       (ArtifactLocationDecoder)
           artifactLocation -> new File("/root", artifactLocation.getRelativePath());
 
-  static final class TestSourceImportConfig extends SourceTestConfig {
-    final boolean isTest;
-
-    public TestSourceImportConfig(boolean isTest) {
-      super(ProjectViewSet.builder().build());
-      this.isTest = isTest;
-    }
-
-    @Override
-    public boolean isTestSource(String relativePath) {
-      return isTest;
-    }
-  }
-
   @Override
   protected void initTest(
       @NotNull Container applicationServices, @NotNull Container projectServices) {
@@ -127,7 +111,6 @@
             project,
             context,
             workspaceRoot,
-            new TestSourceImportConfig(false /* isTest */),
             decoder,
             ImmutableList.of(new WorkspacePath("java/com/google/app")),
             sourceArtifacts,
@@ -160,7 +143,6 @@
             project,
             context,
             workspaceRoot,
-            new TestSourceImportConfig(false),
             decoder,
             ImmutableList.of(new WorkspacePath("java/com/google")),
             sourceArtifacts,
@@ -193,7 +175,6 @@
             project,
             context,
             workspaceRoot,
-            new TestSourceImportConfig(true),
             decoder,
             ImmutableList.of(new WorkspacePath("java/com/google")),
             sourceArtifacts,
@@ -205,7 +186,6 @@
                 .addSource(
                     BlazeSourceDirectory.builder("/root/java/com/google")
                         .setPackagePrefix("com.google")
-                        .setTest(true)
                         .build())
                 .build());
   }
@@ -236,7 +216,6 @@
             project,
             context,
             workspaceRoot,
-            new TestSourceImportConfig(false),
             decoder,
             ImmutableList.of(new WorkspacePath("java/com/google")),
             sourceArtifacts,
@@ -289,7 +268,6 @@
             project,
             context,
             workspaceRoot,
-            new TestSourceImportConfig(false),
             decoder,
             ImmutableList.of(new WorkspacePath("")),
             sourceArtifacts,
@@ -340,7 +318,6 @@
             project,
             context,
             workspaceRoot,
-            new TestSourceImportConfig(false),
             decoder,
             ImmutableList.of(new WorkspacePath("")),
             sourceArtifacts,
@@ -389,7 +366,6 @@
             project,
             context,
             workspaceRoot,
-            new TestSourceImportConfig(false),
             decoder,
             ImmutableList.of(new WorkspacePath("java/com/google")),
             sourceArtifacts,
@@ -435,7 +411,6 @@
             project,
             context,
             workspaceRoot,
-            new TestSourceImportConfig(false),
             decoder,
             ImmutableList.of(new WorkspacePath("java/com/google")),
             sourceArtifacts,
@@ -481,7 +456,6 @@
             project,
             context,
             workspaceRoot,
-            new TestSourceImportConfig(false),
             decoder,
             ImmutableList.of(new WorkspacePath("java/com/google")),
             sourceArtifacts,
@@ -518,7 +492,6 @@
             project,
             context,
             workspaceRoot,
-            new TestSourceImportConfig(false),
             decoder,
             ImmutableList.of(new WorkspacePath("java/com/google")),
             sourceArtifacts,
@@ -551,7 +524,6 @@
             project,
             context,
             workspaceRoot,
-            new TestSourceImportConfig(false),
             decoder,
             ImmutableList.of(new WorkspacePath("java/com/google")),
             sourceArtifacts,
@@ -584,7 +556,6 @@
             project,
             context,
             workspaceRoot,
-            new TestSourceImportConfig(false),
             decoder,
             ImmutableList.of(new WorkspacePath("java/com/org")),
             sourceArtifacts,
@@ -620,7 +591,6 @@
         project,
         context,
         workspaceRoot,
-        new TestSourceImportConfig(false),
         decoder,
         ImmutableList.of(new WorkspacePath("java/com/google")),
         sourceArtifacts,
@@ -646,7 +616,6 @@
         project,
         context,
         workspaceRoot,
-        new TestSourceImportConfig(false),
         decoder,
         ImmutableList.of(new WorkspacePath("java/com/google/my")),
         sourceArtifacts,
@@ -669,7 +638,6 @@
         project,
         context,
         workspaceRoot,
-        new TestSourceImportConfig(false),
         decoder,
         ImmutableList.of(new WorkspacePath("java/com/google")),
         sourceArtifacts,
@@ -717,7 +685,6 @@
             project,
             context,
             workspaceRoot,
-            new TestSourceImportConfig(false),
             decoder,
             ImmutableList.of(new WorkspacePath("java/com/google")),
             sourceArtifacts,
@@ -769,7 +736,6 @@
             project,
             context,
             workspaceRoot,
-            new TestSourceImportConfig(false),
             decoder,
             ImmutableList.of(new WorkspacePath("java/com/google")),
             sourceArtifacts,
@@ -817,7 +783,6 @@
             project,
             context,
             workspaceRoot,
-            new TestSourceImportConfig(false),
             decoder,
             ImmutableList.of(new WorkspacePath("java/com/google")),
             sourceArtifacts,
@@ -872,7 +837,6 @@
             project,
             context,
             workspaceRoot,
-            new TestSourceImportConfig(false),
             decoder,
             ImmutableList.of(new WorkspacePath("java/com/google/android")),
             sourceArtifacts,
@@ -1050,7 +1014,6 @@
             project,
             context,
             workspaceRoot,
-            new TestSourceImportConfig(false),
             getDecoder("/root"),
             ImmutableList.of(new WorkspacePath("java/com/google")),
             sourceArtifacts,
diff --git a/plugin_dev/src/com/google/idea/blaze/plugin/sync/IntellijPluginSyncPlugin.java b/plugin_dev/src/com/google/idea/blaze/plugin/sync/IntellijPluginSyncPlugin.java
index 33ba239..34b06df 100644
--- a/plugin_dev/src/com/google/idea/blaze/plugin/sync/IntellijPluginSyncPlugin.java
+++ b/plugin_dev/src/com/google/idea/blaze/plugin/sync/IntellijPluginSyncPlugin.java
@@ -23,7 +23,10 @@
 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.google.idea.blaze.base.sync.SourceFolderProvider;
 import com.google.idea.blaze.java.sync.JavaLanguageLevelHelper;
+import com.google.idea.blaze.java.sync.model.BlazeJavaSyncData;
+import com.google.idea.blaze.java.sync.projectstructure.JavaSourceFolderProvider;
 import com.intellij.openapi.application.ApplicationManager;
 import com.intellij.openapi.module.ModuleType;
 import com.intellij.openapi.module.StdModuleTypes;
@@ -62,6 +65,15 @@
     return ImmutableSet.of();
   }
 
+  @Nullable
+  @Override
+  public SourceFolderProvider getSourceFolderProvider(BlazeProjectData projectData) {
+    if (!projectData.workspaceLanguageSettings.isWorkspaceType(WorkspaceType.INTELLIJ_PLUGIN)) {
+      return null;
+    }
+    return new JavaSourceFolderProvider(projectData.syncState.get(BlazeJavaSyncData.class));
+  }
+
   @Override
   public void updateProjectSdk(
       Project project,
diff --git a/testing/src/com/google/idea/testing/ServiceHelper.java b/testing/src/com/google/idea/testing/ServiceHelper.java
index 7a8d2ce..2e5457b 100644
--- a/testing/src/com/google/idea/testing/ServiceHelper.java
+++ b/testing/src/com/google/idea/testing/ServiceHelper.java
@@ -15,6 +15,8 @@
  */
 package com.google.idea.testing;
 
+
+import com.intellij.lang.LanguageExtensionPoint;
 import com.intellij.openapi.Disposable;
 import com.intellij.openapi.application.Application;
 import com.intellij.openapi.application.ApplicationManager;
@@ -36,6 +38,20 @@
     Disposer.register(parentDisposable, () -> ep.unregisterExtension(instance));
   }
 
+  /** Unregister all extensions of the given class, for the given extension point. */
+  public static <T> void unregisterLanguageExtensionPoint(
+      String extensionPointKey, Class<T> clazz, Disposable parentDisposable) {
+    ExtensionPoint<LanguageExtensionPoint<T>> ep =
+        Extensions.getRootArea().getExtensionPoint(extensionPointKey);
+    LanguageExtensionPoint<T>[] existingExtensions = ep.getExtensions();
+    for (LanguageExtensionPoint<T> ext : existingExtensions) {
+      if (clazz.getName().equals(ext.implementationClass)) {
+        ep.unregisterExtension(ext);
+        Disposer.register(parentDisposable, () -> ep.registerExtension(ext));
+      }
+    }
+  }
+
   public static <T> void registerApplicationService(
       Class<T> key, T implementation, Disposable parentDisposable) {
     registerComponentInstance(
diff --git a/third_party/BUILD b/third_party/BUILD
index 04f13a6..283fe42 100644
--- a/third_party/BUILD
+++ b/third_party/BUILD
@@ -8,5 +8,5 @@
 
 sh_binary(
     name = "zip",
-    srcs = ["zip/zip.sh"],
+    srcs = ["zip-wrap/zip.sh"],
 )
diff --git a/third_party/zip/zip.sh b/third_party/zip-wrap/zip.sh
similarity index 100%
rename from third_party/zip/zip.sh
rename to third_party/zip-wrap/zip.sh
diff --git a/version.bzl b/version.bzl
index ec651ae..0f144e5 100644
--- a/version.bzl
+++ b/version.bzl
@@ -1,3 +1,3 @@
 """Version of the blaze plugin."""
 
-VERSION = "1.12.6"
+VERSION = "2016.12.05.6"